Skip to content
Merged
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 @@ -91,6 +91,8 @@ All notable changes to this project will be documented in this file.
- Add `link_topologies: Vec<Pubkey>` (capped at 8) and `link_flags: u32` (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` (comma-separated topology names) / `--unicast-drained` flags to `link update`; use `default` as the value to clear all topology assignments
- Extend `tenant get` and `tenant list` to display included topologies; add `--include-topologies` (comma-separated topology names) flag to `tenant update`; use `default` to clear
- Onchain programs
- Add `tunnel_endpoint` field to the `UpdateUser` instruction (`UserUpdateArgs`), allowing the activator to overwrite a user's tunnel endpoint onchain; field is optional and backward compatible via incremental deserialization
- Telemetry
Expand All @@ -100,6 +102,7 @@ All notable changes to this project will be documented in this file.
- 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(sigs) => {
writeln!(out, " backfilled in {} transaction(s)", sigs.len())?;
}
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
1 change: 1 addition & 0 deletions e2e/docker/manager/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ RUN apt-get update && \
apt-get install -y curl perl jq vim iproute2

COPY --from=base /doublezero/bin/doublezero /doublezero/bin/.
COPY --from=base /doublezero/bin/doublezero-admin /doublezero/bin/.
COPY --from=base /doublezero/bin/doublezero_serviceability.so /doublezero/bin/.
COPY --from=base /doublezero/bin/doublezero_telemetry.so /doublezero/bin/.
COPY --from=base /doublezero/bin/doublezero_geolocation.so /doublezero/bin/.
Expand Down
2 changes: 1 addition & 1 deletion e2e/internal/devnet/smartcontract_geolocation.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func (dn *Devnet) InitGeolocationProgramConfigIfNotInitialized(ctx context.Conte
}, docker.NoPrintOnError())
if err != nil {
outputStr := strings.ToLower(string(output))
if strings.Contains(outputStr, "already") || strings.Contains(outputStr, "already in use") {
if strings.Contains(outputStr, "already") || strings.Contains(outputStr, "already in use") || strings.Contains(outputStr, "uninitialized account") {
dn.log.Debug("--> Geolocation program config is already initialized")
return false, nil
}
Expand Down
24 changes: 20 additions & 4 deletions smartcontract/cli/src/link/get.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::{doublezerocommand::CliCommand, validators::validate_code};
use crate::{
doublezerocommand::CliCommand, topology::resolve_topology_names, validators::validate_code,
};
use clap::Args;
use doublezero_program_common::serializer;
use doublezero_sdk::commands::link::get::GetLinkCommand;
Expand Down Expand Up @@ -47,6 +49,8 @@ struct LinkDisplay {
pub status: String,
pub health: String,
pub owner: String,
pub link_topologies: String,
pub unicast_drained: bool,
}

impl GetLinkCliCommand {
Expand All @@ -55,6 +59,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 +100,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 +138,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 +159,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 +171,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 +255,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 +311,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