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" />
|
||||
<PeerStatus v-else-if="activeTab === 'peers'" :peers="peers" />
|
||||
<ChurnControl v-else-if="activeTab === 'churn'" />
|
||||
<FullTable v-else-if="activeTab === 'full-table'" @routes-changed="fetchRoutes" />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@ -63,6 +64,7 @@ import RouteTable from './components/RouteTable.vue'
|
||||
import AnnounceForm from './components/AnnounceForm.vue'
|
||||
import PeerStatus from './components/PeerStatus.vue'
|
||||
import ChurnControl from './components/ChurnControl.vue'
|
||||
import FullTable from './components/FullTable.vue'
|
||||
|
||||
const health = ref(null)
|
||||
const routes = ref([])
|
||||
@ -75,6 +77,7 @@ const tabs = [
|
||||
{ id: 'inject', label: 'Inject' },
|
||||
{ id: 'peers', label: 'Peers' },
|
||||
{ id: 'churn', label: 'Churn' },
|
||||
{ id: 'full-table', label: 'Full Table' },
|
||||
]
|
||||
|
||||
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,12 +48,16 @@ peer_states = {}
|
||||
# ExaBGP command helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_quiet_mode = False
|
||||
|
||||
|
||||
def _send(cmd: str):
|
||||
"""Write a command to ExaBGP via stdout."""
|
||||
with _stdout_lock:
|
||||
sys.stdout.write(cmd + '\n')
|
||||
sys.stdout.flush()
|
||||
log.info('→ ExaBGP: %s', cmd)
|
||||
if not _quiet_mode:
|
||||
log.info('→ ExaBGP: %s', cmd)
|
||||
|
||||
|
||||
def _build_announce(prefix, next_hop='self', as_path=None, communities=None, med=None, local_pref=None):
|
||||
@ -162,7 +166,22 @@ def api_withdraw_all():
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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'])
|
||||
@ -223,6 +242,131 @@ def get_peers():
|
||||
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)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
116
exabgp/inject.py
116
exabgp/inject.py
@ -12,6 +12,9 @@ Usage:
|
||||
inject.py withdraw-all
|
||||
inject.py 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 monitor # live-refresh terminal view
|
||||
|
||||
@ -29,8 +32,8 @@ import requests
|
||||
API = os.environ.get('EXABGP_API', 'http://localhost:5050')
|
||||
|
||||
|
||||
def _post(path, data=None):
|
||||
r = requests.post(f'{API}{path}', json=data or {}, timeout=10)
|
||||
def _post(path, data=None, timeout=10):
|
||||
r = requests.post(f'{API}{path}', json=data or {}, timeout=timeout)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
@ -174,6 +177,101 @@ def cmd_withdraw_scenario(args):
|
||||
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):
|
||||
"""
|
||||
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.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.add_argument('--count', type=int, default=0, metavar='N',
|
||||
help='Number of cycles (0 = infinite)')
|
||||
@ -255,6 +364,9 @@ def main():
|
||||
'withdraw-all': cmd_withdraw_all,
|
||||
'scenario': cmd_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,
|
||||
}
|
||||
|
||||
|
||||
@ -441,6 +441,100 @@ _PATH_DIVERSITY_ROUTES = [
|
||||
# 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 = {
|
||||
'internet_sample': {
|
||||
'description': 'Partial internet table (~80 IPv4 + 14 IPv6 prefixes with realistic AS paths)',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user