2019-04-15 13:36:14 -07:00
|
|
|
//! A command-line executable for monitoring a cluster's gossip plane.
|
2019-04-01 17:12:30 -06:00
|
|
|
|
2021-05-26 09:15:46 -06:00
|
|
|
use {
|
|
|
|
clap::{
|
|
|
|
crate_description, crate_name, value_t, value_t_or_exit, App, AppSettings, Arg, ArgMatches,
|
|
|
|
SubCommand,
|
|
|
|
},
|
|
|
|
solana_clap_utils::{
|
|
|
|
input_parsers::keypair_of,
|
|
|
|
input_validators::{is_keypair_or_ask_keyword, is_port, is_pubkey},
|
|
|
|
},
|
|
|
|
solana_gossip::{contact_info::ContactInfo, gossip_service::discover},
|
|
|
|
solana_sdk::pubkey::Pubkey,
|
2021-07-23 15:25:03 +00:00
|
|
|
solana_streamer::socket::SocketAddrSpace,
|
2021-05-26 09:15:46 -06:00
|
|
|
std::{
|
|
|
|
error,
|
|
|
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
|
|
|
process::exit,
|
|
|
|
time::Duration,
|
|
|
|
},
|
2020-08-22 14:10:19 -07:00
|
|
|
};
|
2019-04-01 17:12:30 -06:00
|
|
|
|
2020-06-18 22:20:52 -07:00
|
|
|
fn parse_matches() -> ArgMatches<'static> {
|
2020-05-13 19:37:40 -07:00
|
|
|
let shred_version_arg = Arg::with_name("shred_version")
|
|
|
|
.long("shred-version")
|
|
|
|
.value_name("VERSION")
|
|
|
|
.takes_value(true)
|
|
|
|
.default_value("0")
|
|
|
|
.help("Filter gossip nodes by this shred version");
|
|
|
|
|
2020-06-18 22:20:52 -07:00
|
|
|
App::new(crate_name!())
|
2019-04-01 17:12:30 -06:00
|
|
|
.about(crate_description!())
|
2020-05-11 15:02:01 -07:00
|
|
|
.version(solana_version::version!())
|
2019-04-22 14:51:20 -07:00
|
|
|
.setting(AppSettings::SubcommandRequiredElseHelp)
|
2021-07-23 15:25:03 +00:00
|
|
|
.arg(
|
|
|
|
Arg::with_name("allow_private_addr")
|
|
|
|
.long("allow-private-addr")
|
|
|
|
.takes_value(false)
|
|
|
|
.help("Allow contacting private ip addresses")
|
|
|
|
.hidden(true),
|
|
|
|
)
|
2019-08-13 10:49:48 -07:00
|
|
|
.subcommand(
|
2020-01-20 23:06:47 -07:00
|
|
|
SubCommand::with_name("rpc-url")
|
2019-08-13 10:49:48 -07:00
|
|
|
.about("Get an RPC URL for the cluster")
|
2019-11-16 14:16:28 -07:00
|
|
|
.arg(
|
|
|
|
Arg::with_name("entrypoint")
|
|
|
|
.short("n")
|
|
|
|
.long("entrypoint")
|
|
|
|
.value_name("HOST:PORT")
|
|
|
|
.takes_value(true)
|
|
|
|
.required(true)
|
|
|
|
.validator(solana_net_utils::is_host_port)
|
|
|
|
.help("Rendezvous with the cluster at this entry point"),
|
|
|
|
)
|
2019-10-24 10:44:05 -07:00
|
|
|
.arg(
|
|
|
|
Arg::with_name("all")
|
|
|
|
.long("all")
|
|
|
|
.takes_value(false)
|
|
|
|
.help("Return all RPC URLs"),
|
|
|
|
)
|
2020-01-02 09:50:48 -07:00
|
|
|
.arg(
|
|
|
|
Arg::with_name("any")
|
|
|
|
.long("any")
|
|
|
|
.takes_value(false)
|
|
|
|
.conflicts_with("all")
|
|
|
|
.help("Return any RPC URL"),
|
|
|
|
)
|
2019-08-13 10:49:48 -07:00
|
|
|
.arg(
|
|
|
|
Arg::with_name("timeout")
|
|
|
|
.long("timeout")
|
|
|
|
.value_name("SECONDS")
|
|
|
|
.takes_value(true)
|
2020-05-15 13:23:40 -07:00
|
|
|
.default_value("15")
|
2019-08-13 10:49:48 -07:00
|
|
|
.help("Timeout in seconds"),
|
|
|
|
)
|
2020-05-13 19:37:40 -07:00
|
|
|
.arg(&shred_version_arg)
|
2019-08-13 10:49:48 -07:00
|
|
|
.setting(AppSettings::DisableVersion),
|
|
|
|
)
|
2019-04-22 14:51:20 -07:00
|
|
|
.subcommand(
|
|
|
|
SubCommand::with_name("spy")
|
2019-05-03 15:00:19 -07:00
|
|
|
.about("Monitor the gossip entrypoint")
|
2019-04-22 14:51:20 -07:00
|
|
|
.setting(AppSettings::DisableVersion)
|
2019-11-16 14:16:28 -07:00
|
|
|
.arg(
|
|
|
|
Arg::with_name("entrypoint")
|
|
|
|
.short("n")
|
|
|
|
.long("entrypoint")
|
|
|
|
.value_name("HOST:PORT")
|
|
|
|
.takes_value(true)
|
|
|
|
.validator(solana_net_utils::is_host_port)
|
2019-11-20 15:21:34 -07:00
|
|
|
.help("Rendezvous with the cluster at this entrypoint"),
|
2019-11-16 14:16:28 -07:00
|
|
|
)
|
2019-10-24 15:35:33 -07:00
|
|
|
.arg(
|
|
|
|
clap::Arg::with_name("gossip_port")
|
|
|
|
.long("gossip-port")
|
2019-11-20 15:21:34 -07:00
|
|
|
.value_name("PORT")
|
2019-10-24 15:35:33 -07:00
|
|
|
.takes_value(true)
|
2019-11-20 15:21:34 -07:00
|
|
|
.validator(is_port)
|
2019-10-24 15:35:33 -07:00
|
|
|
.help("Gossip port number for the node"),
|
|
|
|
)
|
2019-11-20 15:21:34 -07:00
|
|
|
.arg(
|
|
|
|
clap::Arg::with_name("gossip_host")
|
|
|
|
.long("gossip-host")
|
|
|
|
.value_name("HOST")
|
|
|
|
.takes_value(true)
|
|
|
|
.validator(solana_net_utils::is_host)
|
2020-11-12 21:04:15 -08:00
|
|
|
.help("Gossip DNS name or IP address for the node to advertise in gossip \
|
2020-08-22 14:10:19 -07:00
|
|
|
[default: ask --entrypoint, or 127.0.0.1 when --entrypoint is not provided]"),
|
|
|
|
)
|
|
|
|
.arg(
|
|
|
|
Arg::with_name("identity")
|
|
|
|
.short("i")
|
|
|
|
.long("identity")
|
|
|
|
.value_name("PATH")
|
|
|
|
.takes_value(true)
|
|
|
|
.validator(is_keypair_or_ask_keyword)
|
|
|
|
.help("Identity keypair [default: ephemeral keypair]"),
|
2019-11-20 15:21:34 -07:00
|
|
|
)
|
2019-04-22 14:51:20 -07:00
|
|
|
.arg(
|
|
|
|
Arg::with_name("num_nodes")
|
|
|
|
.short("N")
|
|
|
|
.long("num-nodes")
|
|
|
|
.value_name("NUM")
|
|
|
|
.takes_value(true)
|
|
|
|
.conflicts_with("num_nodes_exactly")
|
2019-08-13 10:49:48 -07:00
|
|
|
.help("Wait for at least NUM nodes to be visible"),
|
2019-04-22 14:51:20 -07:00
|
|
|
)
|
|
|
|
.arg(
|
|
|
|
Arg::with_name("num_nodes_exactly")
|
|
|
|
.short("E")
|
|
|
|
.long("num-nodes-exactly")
|
|
|
|
.value_name("NUM")
|
|
|
|
.takes_value(true)
|
2019-08-13 10:49:48 -07:00
|
|
|
.help("Wait for exactly NUM nodes to be visible"),
|
2019-04-22 14:51:20 -07:00
|
|
|
)
|
|
|
|
.arg(
|
|
|
|
Arg::with_name("node_pubkey")
|
|
|
|
.short("p")
|
|
|
|
.long("pubkey")
|
|
|
|
.value_name("PUBKEY")
|
|
|
|
.takes_value(true)
|
2019-08-30 09:27:35 -07:00
|
|
|
.validator(is_pubkey)
|
2019-04-22 14:51:20 -07:00
|
|
|
.help("Public key of a specific node to wait for"),
|
|
|
|
)
|
2020-05-13 19:37:40 -07:00
|
|
|
.arg(&shred_version_arg)
|
2019-04-22 14:51:20 -07:00
|
|
|
.arg(
|
|
|
|
Arg::with_name("timeout")
|
|
|
|
.long("timeout")
|
2019-08-13 10:49:48 -07:00
|
|
|
.value_name("SECONDS")
|
2019-04-22 14:51:20 -07:00
|
|
|
.takes_value(true)
|
2019-08-13 10:49:48 -07:00
|
|
|
.help("Maximum time to wait in seconds [default: wait forever]"),
|
2019-04-22 14:51:20 -07:00
|
|
|
),
|
2019-04-01 17:12:30 -06:00
|
|
|
)
|
2020-06-18 22:20:52 -07:00
|
|
|
.get_matches()
|
|
|
|
}
|
2019-04-01 17:12:30 -06:00
|
|
|
|
2020-06-18 22:20:52 -07:00
|
|
|
fn parse_gossip_host(matches: &ArgMatches, entrypoint_addr: Option<SocketAddr>) -> IpAddr {
|
|
|
|
matches
|
|
|
|
.value_of("gossip_host")
|
|
|
|
.map(|gossip_host| {
|
|
|
|
solana_net_utils::parse_host(gossip_host).unwrap_or_else(|e| {
|
|
|
|
eprintln!("failed to parse gossip-host: {}", e);
|
2019-11-16 14:16:28 -07:00
|
|
|
exit(1);
|
|
|
|
})
|
|
|
|
})
|
2020-06-18 22:20:52 -07:00
|
|
|
.unwrap_or_else(|| {
|
|
|
|
if let Some(entrypoint_addr) = entrypoint_addr {
|
|
|
|
solana_net_utils::get_public_ip_addr(&entrypoint_addr).unwrap_or_else(|err| {
|
|
|
|
eprintln!(
|
|
|
|
"Failed to contact cluster entrypoint {}: {}",
|
|
|
|
entrypoint_addr, err
|
|
|
|
);
|
|
|
|
exit(1);
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn process_spy_results(
|
|
|
|
timeout: Option<u64>,
|
|
|
|
validators: Vec<ContactInfo>,
|
|
|
|
num_nodes: Option<usize>,
|
|
|
|
num_nodes_exactly: Option<usize>,
|
|
|
|
pubkey: Option<Pubkey>,
|
|
|
|
) {
|
|
|
|
if timeout.is_some() {
|
|
|
|
if let Some(num) = num_nodes {
|
|
|
|
if validators.len() < num {
|
|
|
|
let add = if num_nodes_exactly.is_some() {
|
|
|
|
""
|
|
|
|
} else {
|
|
|
|
" or more"
|
|
|
|
};
|
|
|
|
eprintln!(
|
|
|
|
"Error: Insufficient validators discovered. Expecting {}{}",
|
|
|
|
num, add,
|
|
|
|
);
|
|
|
|
exit(1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if let Some(node) = pubkey {
|
2021-04-18 10:27:36 -07:00
|
|
|
if !validators.iter().any(|x| x.id == node) {
|
2020-06-18 22:20:52 -07:00
|
|
|
eprintln!("Error: Could not find node {:?}", node);
|
|
|
|
exit(1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if let Some(num_nodes_exactly) = num_nodes_exactly {
|
|
|
|
if validators.len() > num_nodes_exactly {
|
|
|
|
eprintln!(
|
|
|
|
"Error: Extra nodes discovered. Expecting exactly {}",
|
|
|
|
num_nodes_exactly
|
|
|
|
);
|
|
|
|
exit(1);
|
|
|
|
}
|
2019-04-01 17:12:30 -06:00
|
|
|
}
|
2020-06-18 22:20:52 -07:00
|
|
|
}
|
2019-08-13 10:49:48 -07:00
|
|
|
|
2021-07-29 19:04:45 +00:00
|
|
|
fn process_spy(matches: &ArgMatches, socket_addr_space: SocketAddrSpace) -> std::io::Result<()> {
|
2020-06-18 22:20:52 -07:00
|
|
|
let num_nodes_exactly = matches
|
|
|
|
.value_of("num_nodes_exactly")
|
|
|
|
.map(|num| num.to_string().parse().unwrap());
|
|
|
|
let num_nodes = matches
|
|
|
|
.value_of("num_nodes")
|
|
|
|
.map(|num| num.to_string().parse().unwrap())
|
|
|
|
.or(num_nodes_exactly);
|
|
|
|
let timeout = matches
|
|
|
|
.value_of("timeout")
|
|
|
|
.map(|secs| secs.to_string().parse().unwrap());
|
|
|
|
let pubkey = matches
|
|
|
|
.value_of("node_pubkey")
|
|
|
|
.map(|pubkey_str| pubkey_str.parse::<Pubkey>().unwrap());
|
|
|
|
let shred_version = value_t_or_exit!(matches, "shred_version", u16);
|
2021-06-17 13:51:06 -07:00
|
|
|
let identity_keypair = keypair_of(matches, "identity");
|
2019-04-01 17:12:30 -06:00
|
|
|
|
2020-06-18 22:20:52 -07:00
|
|
|
let entrypoint_addr = parse_entrypoint(matches);
|
2019-11-20 15:21:34 -07:00
|
|
|
|
2020-06-18 22:20:52 -07:00
|
|
|
let gossip_host = parse_gossip_host(matches, entrypoint_addr);
|
2019-11-20 15:21:34 -07:00
|
|
|
|
2020-06-18 22:20:52 -07:00
|
|
|
let gossip_addr = SocketAddr::new(
|
|
|
|
gossip_host,
|
|
|
|
value_t!(matches, "gossip_port", u16).unwrap_or_else(|_| {
|
|
|
|
solana_net_utils::find_available_port_in_range(
|
|
|
|
IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
|
|
|
|
(0, 1),
|
|
|
|
)
|
|
|
|
.expect("unable to find an available gossip port")
|
|
|
|
}),
|
|
|
|
);
|
2021-05-22 12:02:13 -04:00
|
|
|
let discover_timeout = Duration::from_secs(timeout.unwrap_or(u64::MAX));
|
2020-06-18 22:20:52 -07:00
|
|
|
let (_all_peers, validators) = discover(
|
2020-08-22 14:10:19 -07:00
|
|
|
identity_keypair,
|
2020-06-18 22:20:52 -07:00
|
|
|
entrypoint_addr.as_ref(),
|
|
|
|
num_nodes,
|
2021-05-22 12:02:13 -04:00
|
|
|
discover_timeout,
|
|
|
|
pubkey, // find_node_by_pubkey
|
|
|
|
None, // find_node_by_gossip_addr
|
|
|
|
Some(&gossip_addr), // my_gossip_addr
|
2020-06-18 22:20:52 -07:00
|
|
|
shred_version,
|
2021-07-23 15:25:03 +00:00
|
|
|
socket_addr_space,
|
2020-06-18 22:20:52 -07:00
|
|
|
)?;
|
2019-04-01 17:12:30 -06:00
|
|
|
|
2020-06-18 22:20:52 -07:00
|
|
|
process_spy_results(timeout, validators, num_nodes, num_nodes_exactly, pubkey);
|
2019-08-13 10:49:48 -07:00
|
|
|
|
2020-06-18 22:20:52 -07:00
|
|
|
Ok(())
|
|
|
|
}
|
2019-08-13 10:49:48 -07:00
|
|
|
|
2020-06-18 22:20:52 -07:00
|
|
|
fn parse_entrypoint(matches: &ArgMatches) -> Option<SocketAddr> {
|
|
|
|
matches.value_of("entrypoint").map(|entrypoint| {
|
|
|
|
solana_net_utils::parse_host_port(entrypoint).unwrap_or_else(|e| {
|
|
|
|
eprintln!("failed to parse entrypoint address: {}", e);
|
|
|
|
exit(1);
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
2019-08-13 10:49:48 -07:00
|
|
|
|
2021-07-29 19:04:45 +00:00
|
|
|
fn process_rpc_url(
|
|
|
|
matches: &ArgMatches,
|
|
|
|
socket_addr_space: SocketAddrSpace,
|
|
|
|
) -> std::io::Result<()> {
|
2020-06-18 22:20:52 -07:00
|
|
|
let any = matches.is_present("any");
|
|
|
|
let all = matches.is_present("all");
|
2021-06-18 15:34:46 +02:00
|
|
|
let entrypoint_addr = parse_entrypoint(matches);
|
2020-06-18 22:20:52 -07:00
|
|
|
let timeout = value_t_or_exit!(matches, "timeout", u64);
|
|
|
|
let shred_version = value_t_or_exit!(matches, "shred_version", u16);
|
|
|
|
let (_all_peers, validators) = discover(
|
2021-05-22 12:02:13 -04:00
|
|
|
None, // keypair
|
2020-06-18 22:20:52 -07:00
|
|
|
entrypoint_addr.as_ref(),
|
2021-05-22 12:02:13 -04:00
|
|
|
Some(1), // num_nodes
|
|
|
|
Duration::from_secs(timeout),
|
|
|
|
None, // find_node_by_pubkey
|
|
|
|
entrypoint_addr.as_ref(), // find_node_by_gossip_addr
|
|
|
|
None, // my_gossip_addr
|
2020-06-18 22:20:52 -07:00
|
|
|
shred_version,
|
2021-07-23 15:25:03 +00:00
|
|
|
socket_addr_space,
|
2020-06-18 22:20:52 -07:00
|
|
|
)?;
|
|
|
|
|
|
|
|
let rpc_addrs: Vec<_> = validators
|
|
|
|
.iter()
|
|
|
|
.filter_map(|contact_info| {
|
|
|
|
if (any || all || Some(contact_info.gossip) == entrypoint_addr)
|
2021-07-23 15:25:03 +00:00
|
|
|
&& ContactInfo::is_valid_address(&contact_info.rpc, &socket_addr_space)
|
2020-06-18 22:20:52 -07:00
|
|
|
{
|
|
|
|
return Some(contact_info.rpc);
|
2019-10-24 10:44:05 -07:00
|
|
|
}
|
2020-06-18 22:20:52 -07:00
|
|
|
None
|
|
|
|
})
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
if rpc_addrs.is_empty() {
|
|
|
|
eprintln!("No RPC URL found");
|
|
|
|
exit(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
for rpc_addr in rpc_addrs {
|
|
|
|
println!("http://{}", rpc_addr);
|
|
|
|
if any {
|
|
|
|
break;
|
2019-08-13 10:49:48 -07:00
|
|
|
}
|
2020-06-18 22:20:52 -07:00
|
|
|
}
|
2019-04-22 14:51:20 -07:00
|
|
|
|
2020-06-18 22:20:52 -07:00
|
|
|
Ok(())
|
|
|
|
}
|
2019-04-22 14:51:20 -07:00
|
|
|
|
2020-06-18 22:20:52 -07:00
|
|
|
fn main() -> Result<(), Box<dyn error::Error>> {
|
|
|
|
solana_logger::setup_with_default("solana=info");
|
|
|
|
|
|
|
|
let matches = parse_matches();
|
2021-07-29 19:04:45 +00:00
|
|
|
let socket_addr_space = SocketAddrSpace::new(matches.is_present("allow_private_addr"));
|
2020-06-18 22:20:52 -07:00
|
|
|
match matches.subcommand() {
|
|
|
|
("spy", Some(matches)) => {
|
2021-07-29 19:04:45 +00:00
|
|
|
process_spy(matches, socket_addr_space)?;
|
2020-06-18 22:20:52 -07:00
|
|
|
}
|
|
|
|
("rpc-url", Some(matches)) => {
|
2021-07-29 19:04:45 +00:00
|
|
|
process_rpc_url(matches, socket_addr_space)?;
|
2020-06-18 22:20:52 -07:00
|
|
|
}
|
2019-04-22 14:51:20 -07:00
|
|
|
_ => unreachable!(),
|
2019-04-01 17:12:30 -06:00
|
|
|
}
|
2019-04-22 14:51:20 -07:00
|
|
|
|
2019-04-01 17:12:30 -06:00
|
|
|
Ok(())
|
|
|
|
}
|