Skip to content

Commit ed49424

Browse files
smartcontract,client: add topology CLI commands (RFC-18, PR 2/4)
Adds `doublezero link topology {create,delete,clear,backfill,list}` subcommands. - sdk/rs: topology command structs (create, delete, clear, backfill, list) - cli: topology CLI wrappers with clap arg parsing and unit tests - client/doublezero: wire topology subcommands into link command dispatch - clear: auto-discovers tagged links when --links omitted; batches 29/tx
1 parent b6dab42 commit ed49424

18 files changed

Lines changed: 1418 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ All notable changes to this project will be documented in this file.
5353
- Extend `validate_program_account!` migration to remaining user and multicastgroup allowlist processors (`set_bgp_status`, `delete`, `closeaccount`, publisher/subscriber `add`/`remove`)
5454
- Add `OutboundIcmp` target type (`= 2`) to the geolocation onchain program, enabling ICMP-based probing as an alternative to TWAMP for outbound geolocation targets
5555
- Allow pending users with subs to be deleted
56+
- Add `doublezero link topology {create,delete,clear,backfill,list}` subcommands for managing flex-algo topologies; `topology clear` auto-discovers tagged links when `--links` is omitted
5657
- Add `TopologyInfo` onchain account for IS-IS flex-algo link classification: auto-assigned TE admin-group bit (1–62), derived flex-algo number (128 + bit), and constraint type (`include-any`/`include-all`); capped at 62 topologies via `AdminGroupBits` resource extension
5758
- Add `link_topologies: Vec<Pubkey>` (capped at 8) and `link_flags: u8` (bit 0 = unicast-drained) to the `Link` account
5859
- Add `include_topologies` to the `Tenant` account for topology-filtered routing opt-in

client/doublezero/src/cli/link.rs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
use clap::{Args, Subcommand};
2-
use doublezero_cli::link::{
3-
accept::AcceptLinkCliCommand, delete::*, dzx_create::CreateDZXLinkCliCommand, get::*,
4-
latency::LinkLatencyCliCommand, list::*, sethealth::SetLinkHealthCliCommand, update::*,
5-
wan_create::*,
2+
use doublezero_cli::{
3+
link::{
4+
accept::AcceptLinkCliCommand, delete::*, dzx_create::CreateDZXLinkCliCommand, get::*,
5+
latency::LinkLatencyCliCommand, list::*, sethealth::SetLinkHealthCliCommand, update::*,
6+
wan_create::*,
7+
},
8+
topology::{
9+
backfill::BackfillTopologyCliCommand, clear::ClearTopologyCliCommand,
10+
create::CreateTopologyCliCommand, delete::DeleteTopologyCliCommand,
11+
list::ListTopologyCliCommand,
12+
},
613
};
714

815
#[derive(Args, Debug)]
@@ -53,4 +60,27 @@ pub enum LinkCommands {
5360
// Hidden because this is an internal/operational command not intended for general CLI users.
5461
#[clap(hide = true)]
5562
SetHealth(SetLinkHealthCliCommand),
63+
/// Manage link topologies
64+
#[clap()]
65+
Topology(TopologyLinkCommand),
66+
}
67+
68+
#[derive(Args, Debug)]
69+
pub struct TopologyLinkCommand {
70+
#[command(subcommand)]
71+
pub command: TopologyCommands,
72+
}
73+
74+
#[derive(Debug, Subcommand)]
75+
pub enum TopologyCommands {
76+
/// Create a new topology
77+
Create(CreateTopologyCliCommand),
78+
/// Delete a topology
79+
Delete(DeleteTopologyCliCommand),
80+
/// Clear a topology from links
81+
Clear(ClearTopologyCliCommand),
82+
/// Backfill FlexAlgoNodeSegment entries on existing Vpnv4 loopbacks
83+
Backfill(BackfillTopologyCliCommand),
84+
/// List all topologies
85+
List(ListTopologyCliCommand),
5686
}

client/doublezero/src/main.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::cli::{
1717
AirdropCommands, AuthorityCommands, FeatureFlagsCommands, FoundationAllowlistCommands,
1818
GlobalConfigCommands, QaAllowlistCommands,
1919
},
20-
link::LinkCommands,
20+
link::{LinkCommands, TopologyCommands},
2121
location::LocationCommands,
2222
user::UserCommands,
2323
};
@@ -232,6 +232,13 @@ async fn main() -> eyre::Result<()> {
232232
LinkCommands::Latency(args) => args.execute(&client, &mut handle),
233233
LinkCommands::Delete(args) => args.execute(&client, &mut handle),
234234
LinkCommands::SetHealth(args) => args.execute(&client, &mut handle),
235+
LinkCommands::Topology(args) => match args.command {
236+
TopologyCommands::Create(args) => args.execute(&client, &mut handle),
237+
TopologyCommands::Delete(args) => args.execute(&client, &mut handle),
238+
TopologyCommands::Clear(args) => args.execute(&client, &mut handle),
239+
TopologyCommands::Backfill(args) => args.execute(&client, &mut handle),
240+
TopologyCommands::List(args) => args.execute(&client, &mut handle),
241+
},
235242
},
236243
Command::AccessPass(command) => match command.command {
237244
cli::accesspass::AccessPassCommands::Set(args) => args.execute(&client, &mut handle),

smartcontract/cli/src/doublezerocommand.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ use doublezero_sdk::{
9999
remove_administrator::RemoveAdministratorTenantCommand, update::UpdateTenantCommand,
100100
update_payment_status::UpdatePaymentStatusCommand,
101101
},
102+
topology::{
103+
backfill::BackfillTopologyCommand, clear::ClearTopologyCommand,
104+
create::CreateTopologyCommand, delete::DeleteTopologyCommand,
105+
list::ListTopologyCommand,
106+
},
102107
user::{
103108
create::CreateUserCommand, create_subscribe::CreateSubscribeUserCommand,
104109
delete::DeleteUserCommand, get::GetUserCommand, list::ListUserCommand,
@@ -107,7 +112,8 @@ use doublezero_sdk::{
107112
},
108113
telemetry::LinkLatencyStats,
109114
DZClient, Device, DoubleZeroClient, Exchange, GetGlobalConfigCommand, GetGlobalStateCommand,
110-
GlobalConfig, GlobalState, Link, Location, MulticastGroup, ResourceExtensionOwned, User,
115+
GlobalConfig, GlobalState, Link, Location, MulticastGroup, ResourceExtensionOwned,
116+
TopologyInfo, User,
111117
};
112118
use doublezero_serviceability::state::{
113119
accesspass::AccessPass, accountdata::AccountData, contributor::Contributor,
@@ -337,6 +343,15 @@ pub trait CliCommand {
337343
cmd: GetResourceCommand,
338344
) -> eyre::Result<(Pubkey, ResourceExtensionOwned)>;
339345
fn close_resource(&self, cmd: CloseResourceCommand) -> eyre::Result<Signature>;
346+
347+
fn create_topology(&self, cmd: CreateTopologyCommand) -> eyre::Result<(Signature, Pubkey)>;
348+
fn delete_topology(&self, cmd: DeleteTopologyCommand) -> eyre::Result<Signature>;
349+
fn clear_topology(&self, cmd: ClearTopologyCommand) -> eyre::Result<Signature>;
350+
fn backfill_topology(&self, cmd: BackfillTopologyCommand) -> eyre::Result<Signature>;
351+
fn list_topology(
352+
&self,
353+
cmd: ListTopologyCommand,
354+
) -> eyre::Result<HashMap<Pubkey, TopologyInfo>>;
340355
}
341356

342357
pub struct CliCommandImpl<'a> {
@@ -803,4 +818,22 @@ impl CliCommand for CliCommandImpl<'_> {
803818
fn close_resource(&self, cmd: CloseResourceCommand) -> eyre::Result<Signature> {
804819
cmd.execute(self.client)
805820
}
821+
fn create_topology(&self, cmd: CreateTopologyCommand) -> eyre::Result<(Signature, Pubkey)> {
822+
cmd.execute(self.client)
823+
}
824+
fn delete_topology(&self, cmd: DeleteTopologyCommand) -> eyre::Result<Signature> {
825+
cmd.execute(self.client)
826+
}
827+
fn clear_topology(&self, cmd: ClearTopologyCommand) -> eyre::Result<Signature> {
828+
cmd.execute(self.client)
829+
}
830+
fn backfill_topology(&self, cmd: BackfillTopologyCommand) -> eyre::Result<Signature> {
831+
cmd.execute(self.client)
832+
}
833+
fn list_topology(
834+
&self,
835+
cmd: ListTopologyCommand,
836+
) -> eyre::Result<HashMap<Pubkey, TopologyInfo>> {
837+
cmd.execute(self.client)
838+
}
806839
}

smartcontract/cli/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub mod resource;
3030
pub mod subscribe;
3131
pub mod tenant;
3232
pub mod tests;
33+
pub mod topology;
3334
pub mod user;
3435
pub mod util;
3536
pub mod validators;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
use crate::{
2+
doublezerocommand::CliCommand,
3+
requirements::{CHECK_BALANCE, CHECK_ID_JSON},
4+
};
5+
use clap::Args;
6+
use doublezero_sdk::commands::topology::backfill::BackfillTopologyCommand;
7+
use solana_sdk::pubkey::Pubkey;
8+
use std::io::Write;
9+
10+
#[derive(Args, Debug)]
11+
pub struct BackfillTopologyCliCommand {
12+
/// Name of the topology to backfill
13+
#[arg(long)]
14+
pub name: String,
15+
/// Device account pubkeys to backfill (one or more)
16+
#[arg(long = "device", value_name = "PUBKEY")]
17+
pub device_pubkeys: Vec<Pubkey>,
18+
}
19+
20+
impl BackfillTopologyCliCommand {
21+
pub fn execute<C: CliCommand, W: Write>(self, client: &C, out: &mut W) -> eyre::Result<()> {
22+
client.check_requirements(CHECK_ID_JSON | CHECK_BALANCE)?;
23+
24+
if self.device_pubkeys.is_empty() {
25+
return Err(eyre::eyre!(
26+
"at least one --device pubkey is required for backfill"
27+
));
28+
}
29+
30+
let sig = client.backfill_topology(BackfillTopologyCommand {
31+
name: self.name.clone(),
32+
device_pubkeys: self.device_pubkeys,
33+
})?;
34+
35+
writeln!(
36+
out,
37+
"Backfilled topology '{}'. Signature: {}",
38+
self.name, sig
39+
)?;
40+
41+
Ok(())
42+
}
43+
}
44+
45+
#[cfg(test)]
46+
mod tests {
47+
use super::*;
48+
use crate::doublezerocommand::MockCliCommand;
49+
use doublezero_sdk::commands::topology::backfill::BackfillTopologyCommand;
50+
use mockall::predicate::eq;
51+
use solana_sdk::{pubkey::Pubkey, signature::Signature};
52+
use std::io::Cursor;
53+
54+
#[test]
55+
fn test_backfill_topology_execute_success() {
56+
let mut mock = MockCliCommand::new();
57+
let device1 = Pubkey::new_unique();
58+
59+
mock.expect_check_requirements().returning(|_| Ok(()));
60+
mock.expect_backfill_topology()
61+
.with(eq(BackfillTopologyCommand {
62+
name: "unicast-default".to_string(),
63+
device_pubkeys: vec![device1],
64+
}))
65+
.returning(|_| Ok(Signature::new_unique()));
66+
67+
let cmd = BackfillTopologyCliCommand {
68+
name: "unicast-default".to_string(),
69+
device_pubkeys: vec![device1],
70+
};
71+
let mut out = Cursor::new(Vec::new());
72+
let result = cmd.execute(&mock, &mut out);
73+
assert!(result.is_ok());
74+
let output = String::from_utf8(out.into_inner()).unwrap();
75+
assert!(output.contains("Backfilled topology 'unicast-default'."));
76+
}
77+
78+
#[test]
79+
fn test_backfill_topology_requires_at_least_one_device() {
80+
let mut mock = MockCliCommand::new();
81+
mock.expect_check_requirements().returning(|_| Ok(()));
82+
83+
let cmd = BackfillTopologyCliCommand {
84+
name: "unicast-default".to_string(),
85+
device_pubkeys: vec![],
86+
};
87+
let mut out = Cursor::new(Vec::new());
88+
let result = cmd.execute(&mock, &mut out);
89+
assert!(result.is_err());
90+
assert!(result
91+
.unwrap_err()
92+
.to_string()
93+
.contains("at least one --device pubkey is required"));
94+
}
95+
}

0 commit comments

Comments
 (0)