Add json support for feature sets; also print output after feature list (#21905)

* Add json support for feature sets; also print output after feature list

* Move stringifying into Display implementation
This commit is contained in:
Tyera Eulberg
2021-12-14 22:11:08 -07:00
committed by GitHub
parent 7ba27e5cae
commit dcd2854829

View File

@ -5,7 +5,7 @@ use {
}, },
clap::{App, AppSettings, Arg, ArgMatches, SubCommand}, clap::{App, AppSettings, Arg, ArgMatches, SubCommand},
console::style, console::style,
serde::{Deserialize, Serialize}, serde::{Deserialize, Deserializer, Serialize, Serializer},
solana_clap_utils::{input_parsers::*, input_validators::*, keypair::*}, solana_clap_utils::{input_parsers::*, input_validators::*, keypair::*},
solana_cli_output::{QuietDisplay, VerboseDisplay}, solana_cli_output::{QuietDisplay, VerboseDisplay},
solana_client::{client_error::ClientError, rpc_client::RpcClient}, solana_client::{client_error::ClientError, rpc_client::RpcClient},
@ -23,6 +23,7 @@ use {
cmp::Ordering, cmp::Ordering,
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
fmt, fmt,
str::FromStr,
sync::Arc, sync::Arc,
}, },
}; };
@ -104,6 +105,8 @@ impl Ord for CliFeature {
pub struct CliFeatures { pub struct CliFeatures {
pub features: Vec<CliFeature>, pub features: Vec<CliFeature>,
pub feature_activation_allowed: bool, pub feature_activation_allowed: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub cluster_feature_sets: Option<CliClusterFeatureSets>,
#[serde(skip)] #[serde(skip)]
pub inactive: bool, pub inactive: bool,
} }
@ -135,6 +138,11 @@ impl fmt::Display for CliFeatures {
feature.description, feature.description,
)?; )?;
} }
if let Some(feature_sets) = &self.cluster_feature_sets {
write!(f, "{}", feature_sets)?;
}
if self.inactive && !self.feature_activation_allowed { if self.inactive && !self.feature_activation_allowed {
writeln!( writeln!(
f, f,
@ -151,6 +159,191 @@ impl fmt::Display for CliFeatures {
impl QuietDisplay for CliFeatures {} impl QuietDisplay for CliFeatures {}
impl VerboseDisplay for CliFeatures {} impl VerboseDisplay for CliFeatures {}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliClusterFeatureSets {
pub tool_feature_set: u32,
pub feature_sets: Vec<CliFeatureSet>,
#[serde(skip)]
pub stake_allowed: bool,
#[serde(skip)]
pub rpc_allowed: bool,
}
impl fmt::Display for CliClusterFeatureSets {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut tool_feature_set_matches_cluster = false;
let software_versions_title = "Software Version";
let feature_set_title = "Feature Set";
let stake_percent_title = "Stake";
let rpc_percent_title = "RPC";
let mut max_software_versions_len = software_versions_title.len();
let mut max_feature_set_len = feature_set_title.len();
let mut max_stake_percent_len = stake_percent_title.len();
let mut max_rpc_percent_len = rpc_percent_title.len();
let feature_sets: Vec<_> = self
.feature_sets
.iter()
.map(|feature_set_info| {
let me = if self.tool_feature_set == feature_set_info.feature_set {
tool_feature_set_matches_cluster = true;
true
} else {
false
};
let software_versions: Vec<_> = feature_set_info
.software_versions
.iter()
.map(ToString::to_string)
.collect();
let software_versions = software_versions.join(", ");
let feature_set = if feature_set_info.feature_set == 0 {
"unknown".to_string()
} else {
feature_set_info.feature_set.to_string()
};
let stake_percent = format!("{:.2}%", feature_set_info.stake_percent);
let rpc_percent = format!("{:.2}%", feature_set_info.rpc_percent);
max_software_versions_len = max_software_versions_len.max(software_versions.len());
max_feature_set_len = max_feature_set_len.max(feature_set.len());
max_stake_percent_len = max_stake_percent_len.max(stake_percent.len());
max_rpc_percent_len = max_rpc_percent_len.max(rpc_percent.len());
(
software_versions,
feature_set,
stake_percent,
rpc_percent,
me,
)
})
.collect();
if !tool_feature_set_matches_cluster {
writeln!(
f,
"\n{}",
style("To activate features the tool and cluster feature sets must match, select a tool version that matches the cluster")
.bold())?;
} else {
if !self.stake_allowed {
write!(
f,
"\n{}",
style("To activate features the stake must be >= 95%")
.bold()
.red()
)?;
}
if !self.rpc_allowed {
write!(
f,
"\n{}",
style("To activate features the RPC nodes must be >= 95%")
.bold()
.red()
)?;
}
}
writeln!(
f,
"\n\n{}",
style(format!("Tool Feature Set: {}", self.tool_feature_set)).bold()
)?;
writeln!(
f,
"{}",
style(format!(
"{1:<0$} {3:<2$} {5:<4$} {7:<6$}",
max_software_versions_len,
software_versions_title,
max_feature_set_len,
feature_set_title,
max_stake_percent_len,
stake_percent_title,
max_rpc_percent_len,
rpc_percent_title,
))
.bold(),
)?;
for (software_versions, feature_set, stake_percent, rpc_percent, me) in feature_sets {
writeln!(
f,
"{1:<0$} {3:>2$} {5:>4$} {7:>6$} {8}",
max_software_versions_len,
software_versions,
max_feature_set_len,
feature_set,
max_stake_percent_len,
stake_percent,
max_rpc_percent_len,
rpc_percent,
if me { "<-- me" } else { "" },
)?;
}
writeln!(f)
}
}
impl QuietDisplay for CliClusterFeatureSets {}
impl VerboseDisplay for CliClusterFeatureSets {}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliFeatureSet {
software_versions: Vec<CliVersion>,
feature_set: u32,
stake_percent: f64,
rpc_percent: f32,
}
#[derive(Eq, PartialEq, Ord, PartialOrd)]
struct CliVersion(Option<semver::Version>);
impl fmt::Display for CliVersion {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match &self.0 {
None => "unknown".to_string(),
Some(version) => version.to_string(),
};
write!(f, "{}", s)
}
}
impl FromStr for CliVersion {
type Err = semver::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let version_option = if s == "unknown" {
None
} else {
Some(semver::Version::from_str(s)?)
};
Ok(CliVersion(version_option))
}
}
impl Serialize for CliVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for CliVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s: &str = Deserialize::deserialize(deserializer)?;
CliVersion::from_str(s).map_err(serde::de::Error::custom)
}
}
pub trait FeatureSubCommands { pub trait FeatureSubCommands {
fn feature_subcommands(self) -> Self; fn feature_subcommands(self) -> Self;
} }
@ -367,7 +560,10 @@ fn feature_set_stats(rpc_client: &RpcClient) -> Result<FeatureSetStats, ClientEr
} }
// Feature activation is only allowed when 95% of the active stake is on the current feature set // Feature activation is only allowed when 95% of the active stake is on the current feature set
fn feature_activation_allowed(rpc_client: &RpcClient, quiet: bool) -> Result<bool, ClientError> { fn feature_activation_allowed(
rpc_client: &RpcClient,
quiet: bool,
) -> Result<(bool, Option<CliClusterFeatureSets>), ClientError> {
let my_feature_set = solana_version::Version::default().feature_set; let my_feature_set = solana_version::Version::default().feature_set;
let feature_set_stats = feature_set_stats(rpc_client)?; let feature_set_stats = feature_set_stats(rpc_client)?;
@ -383,54 +579,43 @@ fn feature_activation_allowed(rpc_client: &RpcClient, quiet: bool) -> Result<boo
) )
.unwrap_or((false, false)); .unwrap_or((false, false));
if !quiet { let cluster_feature_sets = if quiet {
if feature_set_stats.get(&my_feature_set).is_none() { None
println!( } else {
"{}", let mut feature_sets = feature_set_stats
style("To activate features the tool and cluster feature sets must match, select a tool version that matches the cluster") .into_iter()
.bold()); .map(
} else { |(
if !stake_allowed { feature_set,
print!( FeatureSetStatsEntry {
"\n{}", stake_percent,
style("To activate features the stake must be >= 95%") rpc_nodes_percent: rpc_percent,
.bold() software_versions,
.red() },
); )| {
} CliFeatureSet {
if !rpc_allowed { software_versions: software_versions.into_iter().map(CliVersion).collect(),
print!( feature_set,
"\n{}", stake_percent,
style("To activate features the RPC nodes must be >= 95%") rpc_percent,
.bold() }
.red() },
); )
} .collect::<Vec<_>>();
} feature_sets.sort_by(|l, r| {
println!( match l.software_versions[0]
"\n\n{}", .cmp(&r.software_versions[0])
style(format!("Tool Feature Set: {}", my_feature_set)).bold()
);
let mut feature_set_stats = feature_set_stats.into_iter().collect::<Vec<_>>();
feature_set_stats.sort_by(|l, r| {
match l.1.software_versions[0]
.cmp(&r.1.software_versions[0])
.reverse() .reverse()
{ {
Ordering::Equal => { Ordering::Equal => {
match l match l
.1
.stake_percent .stake_percent
.partial_cmp(&r.1.stake_percent) .partial_cmp(&r.stake_percent)
.unwrap() .unwrap()
.reverse() .reverse()
{ {
Ordering::Equal => { Ordering::Equal => {
l.1.rpc_nodes_percent l.rpc_percent.partial_cmp(&r.rpc_percent).unwrap().reverse()
.partial_cmp(&r.1.rpc_nodes_percent)
.unwrap()
.reverse()
} }
o => o, o => o,
} }
@ -438,96 +623,15 @@ fn feature_activation_allowed(rpc_client: &RpcClient, quiet: bool) -> Result<boo
o => o, o => o,
} }
}); });
Some(CliClusterFeatureSets {
tool_feature_set: my_feature_set,
feature_sets,
stake_allowed,
rpc_allowed,
})
};
let software_versions_title = "Software Version"; Ok((stake_allowed && rpc_allowed, cluster_feature_sets))
let feature_set_title = "Feature Set";
let stake_percent_title = "Stake";
let rpc_percent_title = "RPC";
let mut stats_output = Vec::new();
let mut max_software_versions_len = software_versions_title.len();
let mut max_feature_set_len = feature_set_title.len();
let mut max_stake_percent_len = stake_percent_title.len();
let mut max_rpc_percent_len = rpc_percent_title.len();
for (
feature_set,
FeatureSetStatsEntry {
stake_percent,
rpc_nodes_percent,
software_versions,
},
) in feature_set_stats.into_iter()
{
let me = feature_set == my_feature_set;
let feature_set = if feature_set == 0 {
"unknown".to_string()
} else {
feature_set.to_string()
};
let stake_percent = format!("{:.2}%", stake_percent);
let rpc_percent = format!("{:.2}%", rpc_nodes_percent);
let mut has_unknown = false;
let mut software_versions = software_versions
.iter()
.filter_map(|v| {
if v.is_none() {
has_unknown = true;
}
v.as_ref()
})
.map(ToString::to_string)
.collect::<Vec<_>>();
if has_unknown {
software_versions.push("unknown".to_string());
}
let software_versions = software_versions.join(", ");
max_software_versions_len = max_software_versions_len.max(software_versions.len());
max_feature_set_len = max_feature_set_len.max(feature_set.len());
max_stake_percent_len = max_stake_percent_len.max(stake_percent.len());
max_rpc_percent_len = max_rpc_percent_len.max(rpc_percent.len());
stats_output.push((
software_versions,
feature_set,
stake_percent,
rpc_percent,
me,
));
}
println!(
"{}",
style(format!(
"{1:<0$} {3:<2$} {5:<4$} {7:<6$}",
max_software_versions_len,
software_versions_title,
max_feature_set_len,
feature_set_title,
max_stake_percent_len,
stake_percent_title,
max_rpc_percent_len,
rpc_percent_title,
))
.bold(),
);
for (software_versions, feature_set, stake_percent, rpc_percent, me) in stats_output {
println!(
"{1:<0$} {3:>2$} {5:>4$} {7:>6$} {8}",
max_software_versions_len,
software_versions,
max_feature_set_len,
feature_set,
max_stake_percent_len,
stake_percent,
max_rpc_percent_len,
rpc_percent,
if me { "<-- me" } else { "" },
);
}
println!();
}
Ok(stake_allowed && rpc_allowed)
} }
fn status_from_account(account: Account) -> Option<CliFeatureStatus> { fn status_from_account(account: Account) -> Option<CliFeatureStatus> {
@ -589,10 +693,12 @@ fn process_status(
features.sort_unstable(); features.sort_unstable();
let feature_activation_allowed = feature_activation_allowed(rpc_client, features.len() <= 1)?; let (feature_activation_allowed, cluster_feature_sets) =
feature_activation_allowed(rpc_client, features.len() <= 1)?;
let feature_set = CliFeatures { let feature_set = CliFeatures {
features, features,
feature_activation_allowed, feature_activation_allowed,
cluster_feature_sets,
inactive, inactive,
}; };
Ok(config.output_format.formatted_string(&feature_set)) Ok(config.output_format.formatted_string(&feature_set))
@ -616,7 +722,7 @@ fn process_activate(
} }
} }
if !feature_activation_allowed(rpc_client, false)? { if !feature_activation_allowed(rpc_client, false)?.0 {
match force { match force {
ForceActivation::Almost => ForceActivation::Almost =>
return Err("Add force argument once more to override the sanity check to force feature activation ".into()), return Err("Add force argument once more to override the sanity check to force feature activation ".into()),