Skip to content
Open
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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 40 additions & 19 deletions falcon-lab/src/eos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,29 +64,50 @@ impl EosNode {
LinuxNode(self.0)
}

/// Capture BGP / BFD / routing state via the Arista CLI.
/// Capture BGP / BFD / routing state via the Arista CLI, plus container
/// and host state.
pub async fn collect_diagnostics(&self, d: &Runner, topo: &str) {
let name = self.name(d);
// `Cli -c` takes a single newline-separated script.
const SCRIPT: &str = "enable
show running-config
show ip interface brief
show ip bgp summary
show ip bgp
show ipv6 bgp
show ip route
show ipv6 route
show bfd peers
";
match self.shell(d, SCRIPT).await {
Ok(out) => crate::diagnostics::write_artifact(
for (suffix, cmd) in [
("running-config", "show running-config"),
("ip-interface-brief", "show ip interface brief"),
("ip-bgp-summary", "show ip bgp summary"),
("ip-bgp", "show ip bgp"),
("ipv6-bgp", "show ipv6 bgp"),
("ip-route", "show ip route"),
("ipv6-route", "show ipv6 route"),
("bfd-peers", "show bfd peers"),
] {
let script = format!("enable\n{cmd}");
match self.shell(d, &script).await {
Ok(out) => crate::diagnostics::write_artifact(
d,
topo,
&format!("{name}-{suffix}"),
Some(cmd),
&out,
),
Err(e) => {
slog::warn!(d.log, "diagnostics {name}-{suffix}: {e}")
}
}
}
// Container log plus host-side interface state, so there is something
// to look at even if the CLI is unresponsive.
for (suffix, cmd) in [
("ceos-logs", "docker logs --tail 200 ceos"),
("host-ip-link", "ip -d -s link show"),
("host-ip-addr", "ip -d -s addr show"),
("host-ip-neigh", "ip -d -s neigh show"),
] {
crate::diagnostics::capture(
d,
self.0,
topo,
&format!("{name}-cli"),
None,
&out,
),
Err(e) => slog::warn!(d.log, "diagnostics {name}-cli: {e}"),
&format!("{name}-{suffix}"),
cmd,
)
.await;
}
}

Expand Down
2 changes: 1 addition & 1 deletion falcon-lab/src/frr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ impl FrrNode {
.collect::<Vec<_>>()
.join(" ");
let output = d
.exec(self.0, &format!("vtysh {args}"))
.exec(self.0, &format!("/usr/bin/vtysh {args}"))
.await
.context("vtysh shell failed")?;
Ok(output)
Expand Down
2 changes: 2 additions & 0 deletions falcon-lab/src/illumos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ impl IllumosNode {
("ipadm", "ipadm show-addr"),
("dladm", "dladm show-link"),
("netstat", "netstat -nr"),
("ndp", "ndp -a -n"),
("arp", "arp -a -n"),
] {
crate::diagnostics::capture(
d,
Expand Down
33 changes: 33 additions & 0 deletions falcon-lab/src/mgd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,39 @@ impl MgdNode {
Err(e) => slog::warn!(d.log, "diagnostics {label}: {e}"),
}
}

pub async fn collect_ndp_diagnostics(
&self,
d: &Runner,
client: &Client,
topo: &str,
) {
let name = d.get_node(self.0).name.clone();
for (suffix, result) in [
(
"ndp-manager",
client
.get_ndp_manager_state()
.await
.map(|r| format!("{:#?}", r.into_inner())),
),
(
"ndp-interfaces",
client
.get_ndp_interfaces()
.await
.map(|r| format!("{:#?}", r.into_inner())),
),
] {
let label = format!("{name}-{suffix}");
match result {
Ok(contents) => crate::diagnostics::write_artifact(
d, topo, &label, None, &contents,
),
Err(e) => slog::warn!(d.log, "diagnostics {label}: {e}"),
}
}
}
}

pub async fn wait_for_mgd(
Expand Down
2 changes: 2 additions & 0 deletions falcon-lab/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,13 @@ where
let ox = bt.ox;
let cr1 = bt.cr1;
let cr2 = bt.cr2;
let mgd = bt.mgd.clone();
let topo_name = bt.topo_name.clone();
let result = body(bt).await;
if let Err(e) = &result {
warn!(ad.log, "{topo_name} failed: {e:#}");
collect_diagnostics(&ad, ox, cr1, cr2, &topo_name).await;
ox.collect_ndp_diagnostics(&ad, &mgd, &topo_name).await;
}
result
}
Expand Down
3 changes: 2 additions & 1 deletion mg-api-types/versions/src/latest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,12 @@ pub mod rdb {

pub mod ndp {
pub use crate::v5::ndp::NdpInterface;
pub use crate::v5::ndp::NdpInterfaceSelector;
pub use crate::v5::ndp::NdpManagerState;
pub use crate::v5::ndp::NdpPeer;
pub use crate::v5::ndp::NdpPendingInterface;
pub use crate::v5::ndp::NdpThreadState;

pub use crate::v11::ndp::NdpInterfaceSelector;
}

pub mod rib {
Expand Down
2 changes: 2 additions & 0 deletions mg-api-types/versions/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ pub mod latest;
pub mod v1;
#[path = "v4_over_v6_static_routes/mod.rs"]
pub mod v10;
#[path = "ndp_no_asn/mod.rs"]
pub mod v11;
#[path = "ipv6_basic/mod.rs"]
pub mod v2;
#[path = "switch_identifiers/mod.rs"]
Expand Down
9 changes: 9 additions & 0 deletions mg-api-types/versions/src/ndp_no_asn/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! v11 (NDP_NO_ASN): the NDP admin endpoints are one-per-mgd-instance, so the
//! BGP ASN was dropped from their selectors. Only `NdpInterfaceSelector`
//! changed shape; the response types are unchanged and remain at v5.

pub mod ndp;
11 changes: 11 additions & 0 deletions mg-api-types/versions/src/ndp_no_asn/ndp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
pub struct NdpInterfaceSelector {
pub interface_name: String,
}
44 changes: 39 additions & 5 deletions mg-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ api_versions!([
// | example for the next person.
// v
// (next_int, IDENT),
(11, NDP_NO_ASN),
(10, V4_OVER_V6_STATIC_ROUTES),
(9, ENDPOINT_RENAME),
(8, BGP_SRC_ADDR),
Expand Down Expand Up @@ -1249,30 +1250,63 @@ pub trait MgAdminApi {
#[endpoint {
method = GET,
path = "/ndp/manager",
versions = VERSION_UNNUMBERED..,
versions = VERSION_NDP_NO_ASN..,
}]
async fn get_ndp_manager_state(
rqctx: RequestContext<Self::Context>,
request: Query<latest::bgp::config::AsnSelector>,
) -> Result<HttpResponseOk<latest::ndp::NdpManagerState>, HttpError>;

#[endpoint {
method = GET,
path = "/ndp/manager",
operation_id = "get_ndp_manager_state",
versions = VERSION_UNNUMBERED..VERSION_NDP_NO_ASN,
}]
async fn get_ndp_manager_state_v5(
rqctx: RequestContext<Self::Context>,
_request: Query<v1::bgp::config::AsnSelector>,
) -> Result<HttpResponseOk<latest::ndp::NdpManagerState>, HttpError> {
Self::get_ndp_manager_state(rqctx).await
}

#[endpoint {
method = GET,
path = "/ndp/interfaces",
versions = VERSION_UNNUMBERED..,
versions = VERSION_NDP_NO_ASN..,
}]
async fn get_ndp_interfaces(
rqctx: RequestContext<Self::Context>,
request: Query<latest::bgp::config::AsnSelector>,
) -> Result<HttpResponseOk<Vec<latest::ndp::NdpInterface>>, HttpError>;

#[endpoint {
method = GET,
path = "/ndp/interfaces",
operation_id = "get_ndp_interfaces",
versions = VERSION_UNNUMBERED..VERSION_NDP_NO_ASN,
}]
async fn get_ndp_interfaces_v5(
rqctx: RequestContext<Self::Context>,
request: Query<v1::bgp::config::AsnSelector>,
) -> Result<HttpResponseOk<Vec<latest::ndp::NdpInterface>>, HttpError>;

#[endpoint {
method = GET,
path = "/ndp/interface",
versions = VERSION_UNNUMBERED..,
versions = VERSION_NDP_NO_ASN..,
}]
async fn get_ndp_interface_detail(
rqctx: RequestContext<Self::Context>,
request: Query<latest::ndp::NdpInterfaceSelector>,
) -> Result<HttpResponseOk<latest::ndp::NdpInterface>, HttpError>;

#[endpoint {
method = GET,
path = "/ndp/interface",
operation_id = "get_ndp_interface_detail",
versions = VERSION_UNNUMBERED..VERSION_NDP_NO_ASN,
}]
async fn get_ndp_interface_detail_v5(
rqctx: RequestContext<Self::Context>,
request: Query<v5::ndp::NdpInterfaceSelector>,
) -> Result<HttpResponseOk<latest::ndp::NdpInterface>, HttpError>;
}
49 changes: 16 additions & 33 deletions mgadm/src/ndp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,42 +25,32 @@ pub struct StatusArgs {
#[derive(Subcommand, Debug)]
pub enum StatusCmd {
/// Show NDP manager state
Manager {
#[clap(env)]
asn: u32,
},
Manager,

/// List all NDP-managed interfaces
Interfaces {
#[clap(env)]
asn: u32,
},
Interfaces,

/// Show detailed state for a specific interface
Interface {
interface: String,
#[clap(env)]
asn: u32,
},
Interface { interface: String },
}

pub async fn commands(command: Commands, c: Client) -> Result<()> {
match command {
Commands::Status(args) => match args.command {
StatusCmd::Manager { asn } => ndp_manager_status(asn, c).await?,
StatusCmd::Interfaces { asn } => ndp_interfaces(asn, c).await?,
StatusCmd::Interface { asn, interface } => {
ndp_interface_detail(asn, interface, c).await?
StatusCmd::Manager => ndp_manager_status(c).await?,
StatusCmd::Interfaces => ndp_interfaces(c).await?,
StatusCmd::Interface { interface } => {
ndp_interface_detail(interface, c).await?
}
},
}
Ok(())
}

async fn ndp_manager_status(asn: u32, c: Client) -> Result<()> {
let state = c.get_ndp_manager_state(asn).await?.into_inner();
async fn ndp_manager_status(c: Client) -> Result<()> {
let state = c.get_ndp_manager_state().await?.into_inner();

println_nopipe!("NDP Manager State (ASN {})", asn);
println_nopipe!("NDP Manager State");
println_nopipe!("{}", "=".repeat(60));
println_nopipe!();

Expand Down Expand Up @@ -104,11 +94,11 @@ async fn ndp_manager_status(asn: u32, c: Client) -> Result<()> {
Ok(())
}

async fn ndp_interfaces(asn: u32, c: Client) -> Result<()> {
let interfaces = c.get_ndp_interfaces(asn).await?.into_inner();
async fn ndp_interfaces(c: Client) -> Result<()> {
let interfaces = c.get_ndp_interfaces().await?.into_inner();

if interfaces.is_empty() {
println_nopipe!("No NDP-managed interfaces found for ASN {}", asn);
println_nopipe!("No NDP-managed interfaces found");
return Ok(());
}

Expand All @@ -128,7 +118,7 @@ async fn ndp_interfaces(asn: u32, c: Client) -> Result<()> {
for iface in interfaces {
let (peer_str, reachable_str) = match &iface.discovered_peer {
Some(peer) => {
let addr_str = format!("{}%{}", peer.address, iface.interface);
let addr_str = format!("{}", peer.address);
let reachable = if peer.expired {
"No".red()
} else {
Expand Down Expand Up @@ -173,15 +163,8 @@ async fn ndp_interfaces(asn: u32, c: Client) -> Result<()> {
Ok(())
}

async fn ndp_interface_detail(
asn: u32,
interface: String,
c: Client,
) -> Result<()> {
let detail = c
.get_ndp_interface_detail(asn, &interface)
.await?
.into_inner();
async fn ndp_interface_detail(interface: String, c: Client) -> Result<()> {
let detail = c.get_ndp_interface_detail(&interface).await?.into_inner();

println_nopipe!("NDP State: {}", interface);
println_nopipe!("{}", "=".repeat(60));
Expand Down
Loading