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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ All notable changes to this project will be documented in this file.

### Changes

- Smartcontract
- Rename the `BackfillTopology` instruction to `AssignTopologyNodeSegments` across the program, CLI, and Rust SDK; the instruction discriminant (110) and on-disk semantics are unchanged ([#3648](https://github.com/malbeclabs/doublezero/pull/3648))
- Extend `CreateDeviceInterface` with optional trailing topology PDA accounts (`topology_count: u8`); for Vpnv4 loopbacks under onchain allocation the processor allocates a `FlexAlgoNodeSegment` per topology atomically with interface creation, so newly-provisioned devices no longer need a separate `AssignTopologyNodeSegments` step. The CLI/SDK auto-discover existing topologies and pass them. Topology accounts are validated by program-owner and by first-byte `AccountType::Topology` ([#3648](https://github.com/malbeclabs/doublezero/pull/3648))
- Extend `UpdateDeviceInterface` with `update_topologies: bool` + `topology_count: u8` to reconcile flex-algo node segments against a desired topology set on a Vpnv4 loopback (deallocate removed, allocate added, preserve unchanged); contributor owner or foundation allowlist can call it ([#3648](https://github.com/malbeclabs/doublezero/pull/3648))
- `DeleteDeviceInterface` now deallocates flex-algo node segments alongside the loopback's base SR ID under onchain deallocation ([#3648](https://github.com/malbeclabs/doublezero/pull/3648))
- Controller
- Stamp the default `UNICAST-DEFAULT` topology color on tunnels for users whose access pass has no tenant; previously the color was only resolved inside the tenant lookup branch, leaving tenantless users (the majority on mainnet/testnet) with no color community ([#3648](https://github.com/malbeclabs/doublezero/pull/3648))
- CLI
- `doublezero device interface get` displays `flex_algo_node_segments` as `topology_name:sr_id` rows, falling back to a truncated pubkey when the topology cannot be looked up ([#3648](https://github.com/malbeclabs/doublezero/pull/3648))
- Smartcontract
- Canonicalize the forward-compatible Device interface names: rename the new struct from `NewInterface` to `Interface`, the legacy enum from `Interface` to `InterfaceDeprecated`, the `Device::new_interfaces` field to `Device::interfaces`, and the legacy `Device::interfaces` field to `Device::deprecated_interfaces`. On-disk format and behavior are unchanged — Borsh is positional, so the regenerated device fixture is byte-identical. Mechanical rename across the serviceability program, processors, CLI, sentinel, client, controlplane admin, and the Rust SDK. The activator/ crate was already deleted in an earlier change so the rename does not touch it ([#3663](https://github.com/malbeclabs/doublezero/issues/3663))
- Stop reading `Device::deprecated_interfaces` outside of backward-compat tests. The `delete_device` processor and `Device::validate` now check the canonical `Device::interfaces` vec; the legacy slot is treated as wire-format-only state populated by the deserializer and projected by the serializer. CLI, processor, and Rust SDK construction sites use `..Default::default()` to avoid initializing the legacy field. The unused `UpdateDeviceCommand::deprecated_interfaces` SDK command field is removed. `Device::deprecated_interfaces` itself is retained for backward-compat fixture tests that verify legacy-slot decoding ([#3663](https://github.com/malbeclabs/doublezero/issues/3663))
Expand Down
6 changes: 3 additions & 3 deletions client/doublezero/src/cli/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use doublezero_cli::{
wan_create::*,
},
topology::{
backfill::BackfillTopologyCliCommand, clear::ClearTopologyCliCommand,
assign_node_segments::AssignTopologyNodeSegmentsCliCommand, clear::ClearTopologyCliCommand,
create::CreateTopologyCliCommand, delete::DeleteTopologyCliCommand,
list::ListTopologyCliCommand,
},
Expand Down Expand Up @@ -79,8 +79,8 @@ pub enum TopologyCommands {
Delete(DeleteTopologyCliCommand),
/// Clear a topology from links
Clear(ClearTopologyCliCommand),
/// Backfill FlexAlgoNodeSegment entries on existing Vpnv4 loopbacks
Backfill(BackfillTopologyCliCommand),
/// Assign FlexAlgoNodeSegment entries on Vpnv4 loopbacks
AssignNodeSegments(AssignTopologyNodeSegmentsCliCommand),
/// List all topologies
List(ListTopologyCliCommand),
}
2 changes: 1 addition & 1 deletion client/doublezero/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ async fn main() -> eyre::Result<()> {
TopologyCommands::Create(args) => args.execute(&client, &mut handle),
TopologyCommands::Delete(args) => args.execute(&client, &mut handle),
TopologyCommands::Clear(args) => args.execute(&client, &mut handle),
TopologyCommands::Backfill(args) => args.execute(&client, &mut handle),
TopologyCommands::AssignNodeSegments(args) => args.execute(&client, &mut handle),
TopologyCommands::List(args) => args.execute(&client, &mut handle),
},
},
Expand Down
2 changes: 2 additions & 0 deletions controlplane/controller/internal/controller/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,8 @@ func (c *Controller) updateStateCache(ctx context.Context) error {
if c.featuresConfig != nil && c.featuresConfig.Features.FlexAlgo.Enabled {
tunnel.TenantTopologyColors = resolveTenantColors(tenant.IncludeTopologies, cache.Topologies)
}
} else if c.featuresConfig != nil && c.featuresConfig.Features.FlexAlgo.Enabled {
tunnel.TenantTopologyColors = resolveTenantColors(nil, cache.Topologies)
}
}

Expand Down
13 changes: 8 additions & 5 deletions controlplane/doublezero-admin/src/cli/migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use doublezero_cli::doublezerocommand::CliCommand;
use doublezero_sdk::commands::{
device::list::ListDeviceCommand,
link::{list::ListLinkCommand, update::UpdateLinkCommand},
topology::{backfill::BackfillTopologyCommand, list::ListTopologyCommand},
topology::{
assign_node_segments::AssignTopologyNodeSegmentsCommand, list::ListTopologyCommand,
},
};
use doublezero_serviceability::{pda::get_topology_pda, state::interface::LoopbackType};
use solana_sdk::pubkey::Pubkey;
Expand Down Expand Up @@ -133,10 +135,11 @@ impl FlexAlgoMigrateCliCommand {
)?;

if !self.dry_run {
let result = client.backfill_topology(BackfillTopologyCommand {
name: topology.name.clone(),
device_pubkeys: devices_needing_backfill,
});
let result =
client.assign_topology_node_segments(AssignTopologyNodeSegmentsCommand {
name: topology.name.clone(),
device_pubkeys: devices_needing_backfill,
});
match result {
Ok(sigs) => {
writeln!(out, " backfilled in {} transaction(s)", sigs.len())?;
Expand Down
22 changes: 19 additions & 3 deletions smartcontract/cli/src/device/interface/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ use crate::{
};
use clap::Args;
use doublezero_program_common::{types::network_v4::NetworkV4, validate_iface};
use doublezero_sdk::commands::device::{
get::GetDeviceCommand, interface::create::CreateDeviceInterfaceCommand, list::ListDeviceCommand,
use doublezero_sdk::commands::{
device::{
get::GetDeviceCommand, interface::create::CreateDeviceInterfaceCommand,
list::ListDeviceCommand,
},
topology::list::ListTopologyCommand,
};
use std::io::Write;

Expand Down Expand Up @@ -114,11 +118,21 @@ impl CreateDeviceInterfaceCliCommand {
}
}

// For Vpnv4 loopbacks, discover existing topologies so the onchain program
// can assign FlexAlgoNodeSegment entries atomically during creation.
let loopback_type = self.loopback_type.map(|lt| lt.into()).unwrap_or_default();
let topology_names = if loopback_type == doublezero_sdk::LoopbackType::Vpnv4 {
let topologies = client.list_topology(ListTopologyCommand)?;
topologies.values().map(|t| t.name.clone()).collect()
} else {
vec![]
};

let (signature, _) = client.create_device_interface(CreateDeviceInterfaceCommand {
pubkey: device_pk,
name: self.name.clone(),
ip_net: self.ip_net,
loopback_type: self.loopback_type.map(|lt| lt.into()).unwrap_or_default(),
loopback_type,
interface_cyoa: self.interface_cyoa.map(|ic| ic.into()).unwrap_or_default(),
interface_dia: self.interface_dia.map(|id| id.into()).unwrap_or_default(),
bandwidth: self.bandwidth,
Expand All @@ -127,6 +141,7 @@ impl CreateDeviceInterfaceCliCommand {
routing_mode: self.routing_mode.into(),
vlan_id: self.vlan_id,
user_tunnel_endpoint: self.user_tunnel_endpoint.unwrap_or(false),
topology_names,
})?;
writeln!(out, "Signature: {signature}")?;

Expand Down Expand Up @@ -365,6 +380,7 @@ mod tests {
routing_mode: RoutingMode::Static,
vlan_id: 20,
user_tunnel_endpoint: false,
topology_names: vec![],
}))
.times(1)
.returning(move |_| Ok((signature, device1_pubkey)));
Expand Down
183 changes: 178 additions & 5 deletions smartcontract/cli/src/device/interface/get.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::{doublezerocommand::CliCommand, validators::validate_pubkey_or_code};
use clap::Args;
use doublezero_program_common::validate_iface;
use doublezero_sdk::commands::device::get::GetDeviceCommand;
use doublezero_sdk::commands::{
device::get::GetDeviceCommand, topology::list::ListTopologyCommand,
};
use serde::Serialize;
use std::io::Write;
use tabled::Tabled;
Expand Down Expand Up @@ -33,6 +35,7 @@ struct InterfaceDisplay {
pub ip_net: String,
pub node_segment_idx: u16,
pub user_tunnel_endpoint: bool,
pub flex_algo_node_segments: String,
pub device_pk: String,
}

Expand All @@ -48,6 +51,32 @@ impl GetDeviceInterfaceCliCommand {
.find(|i| i.name.to_lowercase() == self.name.to_lowercase())
.ok_or_else(|| eyre::eyre!("Interface '{}' not found", self.name))?;

// Resolve flex-algo topology PDAs to names. Cache the topology map up
// front so we don't run `list_topology` once per segment; on lookup
// miss (e.g. the topology was deleted), fall back to a truncated pubkey.
let flex_algo_node_segments = if interface.flex_algo_node_segments.is_empty() {
String::new()
} else {
let topology_map = client
.list_topology(ListTopologyCommand)
.unwrap_or_default();
Comment thread
elitegreg marked this conversation as resolved.
interface
.flex_algo_node_segments
.iter()
.map(|seg| {
let label = topology_map
.get(&seg.topology)
.map(|t| t.name.clone())
.unwrap_or_else(|| {
let s = seg.topology.to_string();
format!("{}…", &s[..8.min(s.len())])
});
format!("{}:{}", label, seg.node_segment_idx)
})
.collect::<Vec<_>>()
.join("\n")
};

let display = InterfaceDisplay {
name: interface.name.clone(),
status: interface.status.to_string(),
Expand All @@ -61,6 +90,7 @@ impl GetDeviceInterfaceCliCommand {
ip_net: interface.ip_net.to_string(),
node_segment_idx: interface.node_segment_idx,
user_tunnel_endpoint: interface.user_tunnel_endpoint,
flex_algo_node_segments,
device_pk: device_pk.to_string(),
};

Expand All @@ -71,8 +101,15 @@ impl GetDeviceInterfaceCliCommand {
let headers = InterfaceDisplay::headers();
let fields = display.fields();
let max_len = headers.iter().map(|h| h.len()).max().unwrap_or(0);
let blank = String::new();
for (header, value) in headers.iter().zip(fields.iter()) {
writeln!(out, " {header:<max_len$} | {value}")?;
let mut lines = value.split('\n');
if let Some(first) = lines.next() {
writeln!(out, " {header:<max_len$} | {first}")?;
}
for cont in lines {
writeln!(out, " {blank:<max_len$} | {cont}")?;
}
}
}

Expand All @@ -89,12 +126,13 @@ mod tests {
commands::device::get::GetDeviceCommand, AccountType, Device, DeviceStatus, DeviceType,
Interface,
};
use doublezero_serviceability::state::interface::{
InterfaceStatus, InterfaceType, LoopbackType,
use doublezero_serviceability::state::{
interface::{InterfaceStatus, InterfaceType, LoopbackType},
topology::{FlexAlgoNodeSegment, TopologyConstraint, TopologyInfo},
};
use mockall::predicate;
use solana_sdk::pubkey::Pubkey;
use std::str::FromStr;
use std::{collections::HashMap, str::FromStr};

#[test]
fn test_cli_device_interface_get() {
Expand Down Expand Up @@ -201,4 +239,139 @@ mod tests {
"device_pk row should contain pubkey"
);
}

#[test]
fn test_cli_device_interface_get_displays_flex_algo_node_segments() {
let mut client = create_test_client();

let topo_a = Pubkey::new_unique();
let topo_b = Pubkey::new_unique();
let topo_unknown = Pubkey::new_unique();

let device_pubkey = Pubkey::new_unique();
let device = Device {
account_type: AccountType::Device,
index: 1,
bump_seed: 255,
reference_count: 0,
code: "dz1".to_string(),
contributor_pk: Pubkey::default(),
location_pk: Pubkey::default(),
exchange_pk: Pubkey::new_unique(),
device_type: DeviceType::Hybrid,
public_ip: [1, 2, 3, 4].into(),
dz_prefixes: "1.2.3.4/32".parse().unwrap(),
status: DeviceStatus::Activated,
metrics_publisher_pk: Pubkey::default(),
owner: device_pubkey,
mgmt_vrf: "default".to_string(),
interfaces: vec![Interface {
status: InterfaceStatus::Activated,
name: "Loopback256".to_string(),
interface_type: InterfaceType::Loopback,
loopback_type: LoopbackType::Vpnv4,
interface_cyoa: doublezero_serviceability::state::interface::InterfaceCYOA::None,
interface_dia: doublezero_serviceability::state::interface::InterfaceDIA::None,
bandwidth: 0,
cir: 0,
mtu: 9000,
routing_mode: doublezero_serviceability::state::interface::RoutingMode::Static,
vlan_id: 0,
ip_net: "10.99.0.1/32".parse().unwrap(),
node_segment_idx: 11000,
user_tunnel_endpoint: false,
flex_algo_node_segments: vec![
FlexAlgoNodeSegment {
topology: topo_a,
node_segment_idx: 12001,
},
FlexAlgoNodeSegment {
topology: topo_b,
node_segment_idx: 12002,
},
FlexAlgoNodeSegment {
topology: topo_unknown,
node_segment_idx: 12003,
},
],
..Default::default()
}],
max_users: 255,
users_count: 0,
device_health: doublezero_serviceability::state::device::DeviceHealth::ReadyForUsers,
desired_status:
doublezero_serviceability::state::device::DeviceDesiredStatus::Activated,
unicast_users_count: 0,
multicast_subscribers_count: 0,
max_unicast_users: 0,
max_multicast_subscribers: 0,
reserved_seats: 0,
multicast_publishers_count: 0,
max_multicast_publishers: 0,
..Default::default()
};

client
.expect_get_device()
.with(predicate::eq(GetDeviceCommand {
pubkey_or_code: device_pubkey.to_string(),
}))
.returning(move |_| Ok((device_pubkey, device.clone())));

// Only TOPO-A and TOPO-B are known; topo_unknown is missing from the
// map so it should fall back to a truncated pubkey.
client.expect_list_topology().returning(move |_| {
let mut m = HashMap::new();
m.insert(
topo_a,
TopologyInfo {
account_type: AccountType::Topology,
owner: Pubkey::new_unique(),
bump_seed: 1,
name: "TOPO-A".to_string(),
admin_group_bit: 0,
flex_algo_number: 128,
constraint: TopologyConstraint::IncludeAny,
reference_count: 0,
},
);
m.insert(
topo_b,
TopologyInfo {
account_type: AccountType::Topology,
owner: Pubkey::new_unique(),
bump_seed: 1,
name: "TOPO-B".to_string(),
admin_group_bit: 1,
flex_algo_number: 129,
constraint: TopologyConstraint::IncludeAny,
reference_count: 0,
},
);
Ok(m)
});

let mut output = Vec::new();
let res = GetDeviceInterfaceCliCommand {
device: device_pubkey.to_string(),
name: "Loopback256".to_string(),
json: false,
}
.execute(&client, &mut output);
assert!(res.is_ok(), "{:?}", res.err());
let output_str = String::from_utf8(output).unwrap();
assert!(
output_str.contains("TOPO-A:12001"),
"expected TOPO-A line; got:\n{output_str}"
);
assert!(
output_str.contains("TOPO-B:12002"),
"expected TOPO-B line; got:\n{output_str}"
);
let unknown_short = &topo_unknown.to_string()[..8];
assert!(
output_str.contains(&format!("{unknown_short}…:12003")),
"expected fallback truncated-pubkey line; got:\n{output_str}"
);
}
}
Loading
Loading