Skip to content

Commit

Permalink
Merge pull request #287 from threefoldtech/cli_improvements
Browse files Browse the repository at this point in the history
Cli improvements
  • Loading branch information
LeeSmet authored Jun 5, 2024
2 parents 9ee580e + 167b9e7 commit 15a9423
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 27 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added myceliumd-private binary, which contains private network functionality.
- Added API endpoint to retrieve the public key associated with an IP.
- The CLI can now be used to list, remove or add peers (see `mycelium peers --help`)
- The CLI can now be used to list selected and fallback routes (see `mycelium routes --help`)

### Changed

Expand Down
121 changes: 117 additions & 4 deletions mycelium-api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use core::fmt;
use std::{net::IpAddr, net::SocketAddr, str::FromStr, sync::Arc};

use axum::{
Expand All @@ -6,7 +7,7 @@ use axum::{
routing::{delete, get},
Json, Router,
};
use serde::{Deserialize, Serialize};
use serde::{de, Deserialize, Deserializer, Serialize};
use tokio::sync::Mutex;
use tracing::{debug, error};

Expand All @@ -17,6 +18,8 @@ use mycelium::{
peer_manager::{PeerExists, PeerNotFound, PeerStats},
};

const INFINITE_STR: &str = "infinite";

#[cfg(feature = "message")]
mod message;
#[cfg(feature = "message")]
Expand Down Expand Up @@ -146,6 +149,7 @@ where
}

/// Alias to a [`Metric`](crate::metric::Metric) for serialization in the API.
#[derive(Debug, PartialEq)]
pub enum Metric {
/// Finite metric
Value(u16),
Expand All @@ -155,7 +159,7 @@ pub enum Metric {

/// Info about a route. This uses base types only to avoid having to introduce too many Serialize
/// bounds in the core types.
#[derive(Serialize)]
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Route {
/// We convert the [`subnet`](Subnet) to a string to avoid introducing a bound on the actual
Expand Down Expand Up @@ -269,14 +273,71 @@ impl Serialize for Metric {
S: serde::Serializer,
{
match self {
Self::Infinite => serializer.serialize_str("infinite"),
Self::Infinite => serializer.serialize_str(INFINITE_STR),
Self::Value(v) => serializer.serialize_u16(*v),
}
}
}

impl<'de> Deserialize<'de> for Metric {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct MetricVisitor;

impl<'de> serde::de::Visitor<'de> for MetricVisitor {
type Value = Metric;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string or a u16")
}

fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match value {
INFINITE_STR => Ok(Metric::Infinite),
_ => Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Str(value),
&format!("expected '{}'", INFINITE_STR).as_str(),
)),
}
}

fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
if value <= u16::MAX as u64 {
Ok(Metric::Value(value as u16))
} else {
Err(E::invalid_value(
de::Unexpected::Unsigned(value),
&"expected a non-negative integer within the range of u16",
))
}
}
}
deserializer.deserialize_any(MetricVisitor)
}
}

impl fmt::Display for Metric {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Value(val) => write!(f, "{}", val),
Self::Infinite => write!(f, "{}", INFINITE_STR),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;

#[test]
fn finite_metric_serialization() {
let metric = super::Metric::Value(10);
Expand All @@ -290,6 +351,58 @@ mod tests {
let metric = super::Metric::Infinite;
let s = serde_json::to_string(&metric).expect("can encode infinite metric");

assert_eq!("\"infinite\"", s);
assert_eq!(format!("\"{}\"", INFINITE_STR), s);
}

#[test]
fn test_deserialize_metric() {
// Test deserialization of a Metric::Value
let json_value = json!(20);
let metric: Metric = serde_json::from_value(json_value).unwrap();
assert_eq!(metric, Metric::Value(20));

// Test deserialization of a Metric::Infinite
let json_infinite = json!(INFINITE_STR);
let metric: Metric = serde_json::from_value(json_infinite).unwrap();
assert_eq!(metric, Metric::Infinite);

// Test deserialization of an invalid metric
let json_invalid = json!("invalid");
let result: Result<Metric, _> = serde_json::from_value(json_invalid);
assert!(result.is_err());
}

#[test]
fn test_deserialize_route() {
let json_data = r#"
[
{"subnet":"406:1d77:2438:aa7c::/64","nextHop":"TCP [2a02:1811:d584:7400:c503:ff39:de03:9e44]:45694 <-> [2a01:4f8:212:fa6::2]:9651","metric":20,"seqno":0},
{"subnet":"407:8458:dbf5:4ed7::/64","nextHop":"TCP [2a02:1811:d584:7400:c503:ff39:de03:9e44]:45694 <-> [2a01:4f8:212:fa6::2]:9651","metric":174,"seqno":0},
{"subnet":"408:7ba3:3a4d:808a::/64","nextHop":"TCP [2a02:1811:d584:7400:c503:ff39:de03:9e44]:45694 <-> [2a01:4f8:212:fa6::2]:9651","metric":"infinite","seqno":0}
]
"#;

let routes: Vec<Route> = serde_json::from_str(json_data).unwrap();

assert_eq!(routes[0], Route {
subnet: "406:1d77:2438:aa7c::/64".to_string(),
next_hop: "TCP [2a02:1811:d584:7400:c503:ff39:de03:9e44]:45694 <-> [2a01:4f8:212:fa6::2]:9651".to_string(),
metric: Metric::Value(20),
seqno: 0
});

assert_eq!(routes[1], Route {
subnet: "407:8458:dbf5:4ed7::/64".to_string(),
next_hop: "TCP [2a02:1811:d584:7400:c503:ff39:de03:9e44]:45694 <-> [2a01:4f8:212:fa6::2]:9651".to_string(),
metric: Metric::Value(174),
seqno: 0
});

assert_eq!(routes[2], Route {
subnet: "408:7ba3:3a4d:808a::/64".to_string(),
next_hop: "TCP [2a02:1811:d584:7400:c503:ff39:de03:9e44]:45694 <-> [2a01:4f8:212:fa6::2]:9651".to_string(),
metric: Metric::Infinite,
seqno: 0
});
}
}
4 changes: 2 additions & 2 deletions myceliumd/Cargo.lock

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

2 changes: 2 additions & 0 deletions myceliumd/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod inspect;
mod message;
mod peer;
mod routes;

pub use inspect::inspect;
pub use message::{recv_msg, send_msg};
pub use peer::{add_peers, list_peers, remove_peers};
pub use routes::{list_fallback_routes, list_selected_routes};
46 changes: 28 additions & 18 deletions myceliumd/src/cli/peer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ use std::net::SocketAddr;
use tracing::{debug, error};

/// List the peers the current node is connected to
pub async fn list_peers(server_addr: SocketAddr) -> Result<(), Box<dyn std::error::Error>> {
pub async fn list_peers(
server_addr: SocketAddr,
json_print: bool,
) -> Result<(), Box<dyn std::error::Error>> {
// Make API call
let request_url = format!("http://{server_addr}/api/v1/admin/peers");
match reqwest::get(&request_url).await {
Expand All @@ -21,26 +24,33 @@ pub async fn list_peers(server_addr: SocketAddr) -> Result<(), Box<dyn std::erro
return Err(e.into());
}
Ok(peers) => {
let mut table = Table::new();
table.add_row(row![
"Protocol",
"Socket",
"Type",
"Connection",
"Rx total",
"Tx total"
]);
for peer in peers.iter() {
if json_print {
// Print peers in JSON format
let json_output = serde_json::to_string_pretty(&peers)?;
println!("{json_output}");
} else {
// Print peers in table format
let mut table = Table::new();
table.add_row(row![
peer.endpoint.proto(),
peer.endpoint.address(),
peer.pt,
peer.connection_state,
format_bytes(peer.rx_bytes),
format_bytes(peer.tx_bytes),
"Protocol",
"Socket",
"Type",
"Connection",
"Rx total",
"Tx total"
]);
for peer in peers.iter() {
table.add_row(row![
peer.endpoint.proto(),
peer.endpoint.address(),
peer.pt,
peer.connection_state,
format_bytes(peer.rx_bytes),
format_bytes(peer.tx_bytes),
]);
}
table.printstd();
}
table.printstd();
}
}
}
Expand Down
84 changes: 84 additions & 0 deletions myceliumd/src/cli/routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use mycelium_api::Route;
use prettytable::{row, Table};
use std::net::SocketAddr;

use tracing::{debug, error};

pub async fn list_selected_routes(
server_addr: SocketAddr,
json_print: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let request_url = format!("http://{server_addr}/api/v1/admin/routes/selected");
match reqwest::get(&request_url).await {
Err(e) => {
error!("Failed to retrieve selected routes");
return Err(e.into());
}
Ok(resp) => {
debug!("Listing selected routes");

if json_print {
// API call returns routes in JSON format by default
let selected_routes = resp.text().await?;
println!("{selected_routes}");
} else {
// Print routes in table format
let routes: Vec<Route> = resp.json().await?;
let mut table = Table::new();
table.add_row(row!["Subnet", "Next Hop", "Metric", "Seq No"]);

for route in routes.iter() {
table.add_row(row![
&route.subnet,
&route.next_hop,
route.metric,
route.seqno,
]);
}

table.printstd();
}
}
}

Ok(())
}

pub async fn list_fallback_routes(
server_addr: SocketAddr,
json_print: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let request_url = format!("http://{server_addr}/api/v1/admin/routes/fallback");
match reqwest::get(&request_url).await {
Err(e) => {
error!("Failed to retrieve fallback routes");
return Err(e.into());
}
Ok(resp) => {
debug!("Listing fallback routes");

if json_print {
// API call returns routes in JSON format by default
let fallback_routes = resp.text().await?;
println!("{fallback_routes}");
} else {
// Print routes in table format
let routes: Vec<Route> = resp.json().await?;
let mut table = Table::new();
table.add_row(row!["Subnet", "Next Hop", "Metric", "Seq No"]);

for route in routes.iter() {
table.add_row(row![
&route.subnet,
&route.next_hop,
route.metric,
route.seqno,
]);
}

table.printstd();
}
}
}
Ok(())
}
Loading

0 comments on commit 15a9423

Please sign in to comment.