Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,16 @@ All notable changes to this project will be documented in this file.
- Add `link_topologies: Vec<Pubkey>` (capped at 8) and `link_flags: u8` (bit 0 = unicast-drained) to the `Link` account
- Add `include_topologies` to the `Tenant` account for topology-filtered routing opt-in
- Enforce UNICAST-DEFAULT topology existence as a precondition for link activation
- Extend `link get` and `link list` to display topology assignments and drain status; add `--link-topology <name>` filter to `link list` and `--link-topology`/`--unicast-drained` flags to `link update`
- Extend `tenant get` and `tenant list` to display included topologies; add `--include-topologies` flag to `tenant update`
- Telemetry
- Device telemetry agent now posts `agent_version` and `agent_commit` in the `DeviceLatencySamplesHeader` when initializing new sample accounts, enabling version attribution of onchain telemetry data
- Add optional TLS support to state-ingest server via `--tls-cert-file` and `--tls-key-file` flags; when set, the server listens on both HTTP (`:8080`) and HTTPS (`:8443`) simultaneously
- Remove `--additional-child-probes` CLI flag from telemetry-agent; child geoprobe discovery now relies entirely on the onchain Geolocation program
- Add BGP status submitter: on each tick, reads BGP socket state from the device namespace, maps each activated user to their tunnel peer IP, and submits `SetUserBGPStatus` onchain; supports a configurable down grace period and periodic keepalive refresh; enabled via `--bgp-status-enable` with `--bgp-status-interval`, `--bgp-status-refresh-interval`, and `--bgp-status-down-grace-period` flags
- Tools
- Add `IsRetryableFunc` field to `RetryOptions` for configurable retry criteria in the Solana JSON-RPC client; add `"rate limited"` string match and RPC code `-32429` to the default implementation
- Add `doublezero-admin migrate flex-algo [--dry-run]` command to backfill link topology assignments and VPNv4 loopback flex-algo node segments across all existing devices and links
- Geolocation
- Standardize CLI flag naming: probe mutation commands use `--probe` (was `--code`) accepting pubkey or code; rename `--signing-keypair` → `--signing-pubkey` and `--target-pk` → `--target-signing-pubkey`; add `--json-compact` to `get` commands
- geoprobe-target can now store LocationOffset messages in ClickHouse
Expand Down
7 changes: 5 additions & 2 deletions controlplane/doublezero-admin/src/cli/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use super::{multicast::MulticastCliCommand, sentinel::SentinelCliCommand};
use crate::cli::{
accesspass::AccessPassCliCommand, config::ConfigCliCommand, contributor::ContributorCliCommand,
device::DeviceCliCommand, exchange::ExchangeCliCommand, globalconfig::GlobalConfigCliCommand,
link::LinkCliCommand, location::LocationCliCommand, permission::PermissionCliCommand,
tenant::TenantCliCommand, user::UserCliCommand,
link::LinkCliCommand, location::LocationCliCommand, migrate::MigrateCliCommand,
permission::PermissionCliCommand, tenant::TenantCliCommand, user::UserCliCommand,
};
use clap::{Args, Subcommand};
use clap_complete::Shell;
Expand Down Expand Up @@ -69,6 +69,9 @@ pub enum Command {
/// Sentinel admin commands
#[command()]
Sentinel(SentinelCliCommand),
/// Backfill link topologies and report Vpnv4 loopback gaps (RFC-18 migration)
#[command()]
Migrate(MigrateCliCommand),
/// Export all data to files
#[command()]
Export(ExportCliCommand),
Expand Down
180 changes: 180 additions & 0 deletions controlplane/doublezero-admin/src/cli/migrate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
use clap::{Args, Subcommand};
use doublezero_cli::doublezerocommand::CliCommand;
use doublezero_sdk::commands::{
device::list::ListDeviceCommand,
link::{list::ListLinkCommand, update::UpdateLinkCommand},
topology::{backfill::BackfillTopologyCommand, list::ListTopologyCommand},
};
use doublezero_serviceability::{pda::get_topology_pda, state::interface::LoopbackType};
use solana_sdk::pubkey::Pubkey;
use std::io::Write;

#[derive(Args, Debug)]
pub struct MigrateCliCommand {
#[command(subcommand)]
pub command: MigrateCommands,
}

#[derive(Debug, Subcommand)]
pub enum MigrateCommands {
/// Backfill link topologies and Vpnv4 loopback FlexAlgoNodeSegments (RFC-18 migration)
FlexAlgo(FlexAlgoMigrateCliCommand),
}

#[derive(Args, Debug)]
pub struct FlexAlgoMigrateCliCommand {
/// Print what would be changed without submitting transactions
#[arg(long, default_value_t = false)]
pub dry_run: bool,
}

impl FlexAlgoMigrateCliCommand {
pub fn execute<C: CliCommand, W: Write>(&self, client: &C, out: &mut W) -> eyre::Result<()> {
let program_id = client.get_program_id();

// Verify UNICAST-DEFAULT topology PDA exists on chain.
let (unicast_default_pda, _) = get_topology_pda(&program_id, "unicast-default");
client
.get_account(unicast_default_pda)
.map_err(|_| eyre::eyre!("UNICAST-DEFAULT topology PDA {unicast_default_pda} not found on chain — cannot proceed"))?;

// ── Part 1: link topology backfill ───────────────────────────────────────

let links = client.list_link(ListLinkCommand)?;
let mut links_tagged = 0u32;
let mut links_needing_tag = 0u32;
let mut links_skipped = 0u32;

let mut link_entries: Vec<(Pubkey, _)> = links.into_iter().collect();
link_entries.sort_by_key(|(pk, _)| pk.to_string());

for (pubkey, link) in &link_entries {
if link.link_topologies.is_empty() {
links_needing_tag += 1;
writeln!(
out,
" [link] {pubkey} ({}) — would tag UNICAST-DEFAULT",
link.code
)?;
if !self.dry_run {
let result = client.update_link(UpdateLinkCommand {
pubkey: *pubkey,
code: None,
contributor_pk: None,
tunnel_type: None,
bandwidth: None,
mtu: None,
delay_ns: None,
jitter_ns: None,
delay_override_ns: None,
status: None,
desired_status: None,
tunnel_id: None,
tunnel_net: None,
link_topologies: Some(vec![unicast_default_pda]),
unicast_drained: None,
});
match result {
Ok(sig) => {
links_tagged += 1;
writeln!(out, " tagged: {sig}")?;
}
Err(e) => {
writeln!(out, " WARNING: failed to tag {pubkey}: {e}")?;
}
}
}
} else {
links_skipped += 1;
}
}

// ── Part 2: Vpnv4 loopback FlexAlgoNodeSegment backfill ─────────────────

let topologies = client.list_topology(ListTopologyCommand)?;
let mut topology_entries: Vec<(Pubkey, _)> = topologies.into_iter().collect();
topology_entries.sort_by_key(|(pk, _)| pk.to_string());

let devices = client.list_device(ListDeviceCommand)?;
let mut device_entries: Vec<(Pubkey, _)> = devices.into_iter().collect();
device_entries.sort_by_key(|(pk, _)| pk.to_string());

let mut topologies_backfilled = 0u32;
let mut topologies_skipped = 0u32;

for (topology_pubkey, topology) in &topology_entries {
let mut devices_needing_backfill: Vec<Pubkey> = Vec::new();

for (device_pubkey, device) in &device_entries {
let needs_backfill = device.interfaces.iter().any(|iface| {
let current = iface.into_current_version();
current.loopback_type == LoopbackType::Vpnv4
&& !current
.flex_algo_node_segments
.iter()
.any(|s| s.topology == *topology_pubkey)
});
if needs_backfill {
devices_needing_backfill.push(*device_pubkey);
}
}

if devices_needing_backfill.is_empty() {
topologies_skipped += 1;
continue;
}

topologies_backfilled += 1;
writeln!(
out,
" [topology] {} ({}) — {} device(s) need backfill",
topology.name,
topology_pubkey,
devices_needing_backfill.len()
)?;

if !self.dry_run {
let result = client.backfill_topology(BackfillTopologyCommand {
name: topology.name.clone(),
device_pubkeys: devices_needing_backfill,
});
match result {
Ok(sig) => {
writeln!(out, " backfilled: {sig}")?;
}
Err(e) => {
writeln!(
out,
" WARNING: failed to backfill topology {}: {e}",
topology.name
)?;
}
}
}
}

// ── Summary ──────────────────────────────────────────────────────────────

let dry_run_suffix = if self.dry_run {
" [DRY RUN — no changes made]"
} else {
""
};
let tagged_summary = if self.dry_run {
format!("{links_needing_tag} link(s) would be tagged")
} else {
format!("{links_tagged} link(s) tagged")
};
let loopback_summary = if self.dry_run {
format!("{topologies_backfilled} topology(s) would be backfilled")
} else {
format!("{topologies_backfilled} topology(s) backfilled")
};
writeln!(
out,
"\nMigration complete: {tagged_summary}, {links_skipped} link(s) skipped; {loopback_summary}, {topologies_skipped} topology(s) already complete{dry_run_suffix}"
)?;

Ok(())
}
}
1 change: 1 addition & 0 deletions controlplane/doublezero-admin/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod exchange;
pub mod globalconfig;
pub mod link;
pub mod location;
pub mod migrate;
pub mod multicast;
pub mod multicastgroup;
pub mod permission;
Expand Down
3 changes: 3 additions & 0 deletions controlplane/doublezero-admin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,9 @@ async fn main() -> eyre::Result<()> {
}
},

Command::Migrate(args) => match args.command {
cli::migrate::MigrateCommands::FlexAlgo(cmd) => cmd.execute(&client, &mut handle),
},
Command::Export(args) => args.execute(&client, &mut handle),
Command::Keygen(args) => args.execute(&client, &mut handle),
Command::Log(args) => args.execute(&dzclient, &mut handle),
Expand Down
44 changes: 39 additions & 5 deletions smartcontract/cli/src/link/get.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use crate::{doublezerocommand::CliCommand, validators::validate_code};
use clap::Args;
use doublezero_program_common::serializer;
use doublezero_sdk::commands::link::get::GetLinkCommand;
use doublezero_sdk::{commands::link::get::GetLinkCommand, TopologyInfo};
use serde::Serialize;
use solana_sdk::pubkey::Pubkey;
use std::io::Write;
use std::{collections::HashMap, io::Write};
use tabled::Tabled;

#[derive(Args, Debug)]
Expand Down Expand Up @@ -47,6 +47,28 @@ struct LinkDisplay {
pub status: String,
pub health: String,
pub owner: String,
pub link_topologies: String,
pub unicast_drained: bool,
}

fn resolve_topology_names(
pubkeys: &[Pubkey],
topology_map: &HashMap<Pubkey, TopologyInfo>,
) -> String {
if pubkeys.is_empty() {
"default".to_string()
} else {
pubkeys
.iter()
.map(|pk| {
topology_map
.get(pk)
.map(|t| t.name.clone())
.unwrap_or_else(|| pk.to_string())
})
.collect::<Vec<_>>()
.join(", ")
}
}

impl GetLinkCliCommand {
Expand All @@ -55,6 +77,10 @@ impl GetLinkCliCommand {
pubkey_or_code: self.code,
})?;

let topology_map = client
.list_topology(doublezero_sdk::commands::topology::list::ListTopologyCommand)
.unwrap_or_default();

let display = LinkDisplay {
account: pubkey.to_string(),
code: link.code,
Expand Down Expand Up @@ -92,6 +118,10 @@ impl GetLinkCliCommand {
status: link.status.to_string(),
health: link.link_health.to_string(),
owner: link.owner.to_string(),
link_topologies: resolve_topology_names(&link.link_topologies, &topology_map),
unicast_drained: link.link_flags
& doublezero_serviceability::state::link::LINK_FLAG_UNICAST_DRAINED
!= 0,
};

if self.json {
Expand Down Expand Up @@ -126,6 +156,7 @@ mod tests {
};
use mockall::predicate;
use solana_sdk::pubkey::Pubkey;
use std::collections::HashMap;

#[test]
fn test_cli_link_get() {
Expand All @@ -146,7 +177,7 @@ mod tests {
side_z_pk: device2_pk,
link_type: LinkLinkType::WAN,
bandwidth: 1000000000,
mtu: 9000,
mtu: 1500,
delay_ns: 10000000000,
jitter_ns: 5000000000,
delay_override_ns: 0,
Expand All @@ -158,7 +189,7 @@ mod tests {
side_z_iface_name: "eth1".to_string(),
link_health: doublezero_serviceability::state::link::LinkHealth::ReadyForService,
desired_status: doublezero_serviceability::state::link::LinkDesiredStatus::Activated,
link_topologies: vec![],
link_topologies: Vec::new(),
link_flags: 0,
};

Expand Down Expand Up @@ -242,6 +273,9 @@ mod tests {
pubkey_or_code: device2_pk.to_string(),
}))
.returning(move |_| Ok((device2_pk, device2.clone())));
client
.expect_list_topology()
.returning(|_| Ok(HashMap::new()));

// Expected failure
let mut output = Vec::new();
Expand Down Expand Up @@ -295,7 +329,7 @@ mod tests {
assert_eq!(json["status"].as_str().unwrap(), "activated");
assert_eq!(json["tunnel_type"].as_str().unwrap(), "WAN");
assert_eq!(json["bandwidth"].as_u64().unwrap(), 1_000_000_000);
assert_eq!(json["mtu"].as_u64().unwrap(), 9000);
assert_eq!(json["mtu"].as_u64().unwrap(), 1500);
assert_eq!(json["contributor"].as_str().unwrap(), "test-contributor");
assert_eq!(json["side_a"].as_str().unwrap(), "side-a-device");
assert_eq!(json["side_z"].as_str().unwrap(), "side-z-device");
Expand Down
Loading
Loading