Skip to content

Commit

Permalink
feat(sns-cli): Add sns-lint command
Browse files Browse the repository at this point in the history
  • Loading branch information
anchpop committed Sep 27, 2024
1 parent a34cbd9 commit 5206cfc
Show file tree
Hide file tree
Showing 17 changed files with 362 additions and 35 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions rs/nervous_system/agent/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ DEPENDENCIES = [
"//rs/nns/constants",
"//rs/nns/sns-wasm",
"//rs/sns/governance",
"//rs/sns/root",
"//rs/types/base_types",
"@crate_index//:anyhow",
"@crate_index//:candid",
Expand Down
1 change: 1 addition & 0 deletions rs/nervous_system/agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ ic-nns-constants = { path = "../../nns/constants" }
ic-sns-wasm = { path = "../../nns/sns-wasm" }
ic-sns-governance = { path = "../../sns/governance" }
pocket-ic = { path = "../../../packages/pocket-ic" }
ic-sns-root = { path = "../../sns/root" }
serde = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
Expand Down
2 changes: 1 addition & 1 deletion rs/nervous_system/agent/src/nns/sns_wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use tokio::process::Command;

use crate::CallCanisters;

pub async fn query_sns_upgrade_steps<C: CallCanisters>(
pub async fn query_mainline_sns_upgrade_steps<C: CallCanisters>(
agent: &C,
) -> Result<ListUpgradeStepsResponse, C::Error> {
let request = ListUpgradeStepsRequest {
Expand Down
14 changes: 13 additions & 1 deletion rs/nervous_system/agent/src/sns/governance.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use crate::CallCanisters;
use ic_base_types::PrincipalId;
use ic_sns_governance::pb::v1::{GetMetadataRequest, GetMetadataResponse};
use ic_sns_governance::pb::v1::{
GetMetadataRequest, GetMetadataResponse, GetRunningSnsVersionRequest,
GetRunningSnsVersionResponse,
};
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Deserialize, Serialize)]
Expand All @@ -15,4 +18,13 @@ impl GovernanceCanister {
) -> Result<GetMetadataResponse, C::Error> {
agent.call(self.canister_id, GetMetadataRequest {}).await
}

pub async fn version<C: CallCanisters>(
&self,
agent: &C,
) -> Result<GetRunningSnsVersionResponse, C::Error> {
agent
.call(self.canister_id, GetRunningSnsVersionRequest {})
.await
}
}
23 changes: 23 additions & 0 deletions rs/nervous_system/agent/src/sns/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ pub mod ledger;
pub mod root;
pub mod swap;

use anyhow::Result;
use ic_nns_constants::SNS_WASM_CANISTER_ID;
use ic_sns_wasm::pb::v1::{ListUpgradeStepsRequest, ListUpgradeStepsResponse, SnsVersion};
use serde::{Deserialize, Serialize};

use crate::CallCanisters;

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Sns {
pub ledger: ledger::LedgerCanister,
Expand All @@ -14,6 +20,23 @@ pub struct Sns {
pub root: root::RootCanister,
}

impl Sns {
pub async fn remaining_upgrade_steps<C: CallCanisters>(
&self,
agent: &C,
) -> Result<ListUpgradeStepsResponse, C::Error> {
let version = self.governance.version(agent).await?;
let list_upgrade_steps_request = ListUpgradeStepsRequest {
limit: 0,
sns_governance_canister_id: Some(self.governance.canister_id),
starting_at: version.deployed_version.map(SnsVersion::from),
};
agent
.call(SNS_WASM_CANISTER_ID, list_upgrade_steps_request)
.await
}
}

impl TryFrom<ic_sns_wasm::pb::v1::DeployedSns> for Sns {
type Error = String;

Expand Down
19 changes: 19 additions & 0 deletions rs/nervous_system/agent/src/sns/root.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
use ic_base_types::PrincipalId;
use ic_sns_root::{GetSnsCanistersSummaryRequest, GetSnsCanistersSummaryResponse};
use serde::{Deserialize, Serialize};

use crate::CallCanisters;

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct RootCanister {
pub canister_id: PrincipalId,
}

impl RootCanister {
pub async fn sns_canisters_summary<C: CallCanisters>(
&self,
agent: &C,
) -> Result<GetSnsCanistersSummaryResponse, C::Error> {
agent
.call(
self.canister_id,
GetSnsCanistersSummaryRequest {
update_canister_list: None,
},
)
.await
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ async fn main() -> Result<()> {
.into_iter()
.collect::<Result<Vec<CanisterUpdate>>>()?;

let sns_upgrade_steps = sns_wasm::query_sns_upgrade_steps(&agent).await?;
let sns_upgrade_steps = sns_wasm::query_mainline_sns_upgrade_steps(&agent).await?;
let latest_sns_version = &sns_upgrade_steps
.steps
.last()
Expand Down
5 changes: 4 additions & 1 deletion rs/sns/cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
deploy::DirectSnsDeployerForTests, init_config_file::InitConfigFileArgs,
deploy::DirectSnsDeployerForTests, init_config_file::InitConfigFileArgs, lint::LintArgs,
neuron_id_to_candid_subaccount::NeuronIdToCandidSubaccountArgs,
prepare_canisters::PrepareCanistersArgs, propose::ProposeArgs,
};
Expand Down Expand Up @@ -31,6 +31,7 @@ use tempfile::NamedTempFile;

pub mod deploy;
pub mod init_config_file;
pub mod lint;
pub mod list;
pub mod neuron_id_to_candid_subaccount;
pub mod prepare_canisters;
Expand Down Expand Up @@ -77,6 +78,8 @@ pub enum SubCommand {
NeuronIdToCandidSubaccount(NeuronIdToCandidSubaccountArgs),
/// List SNSes
List(list::ListArgs),
/// Check SNSes for warnings and errors.
Lint(LintArgs),
}

impl CliArgs {
Expand Down
176 changes: 176 additions & 0 deletions rs/sns/cli/src/lint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
use crate::table::{as_table, TableRow};
use crate::utils::{get_snses_with_metadata, SnsWithMetadata};
use anyhow::Result;
use clap::Parser;
use futures::{stream, StreamExt};
use ic_agent::Agent;
use ic_nervous_system_agent::nns::sns_wasm;
use ic_sns_root::types::SnsCanisterType;
use itertools::Itertools;
use serde::{Deserialize, Serialize};

/// The arguments used to configure the lint command
#[derive(Debug, Parser)]
pub struct LintArgs {
/// Output the SNS information as JSON (instead of a human-friendly table).
#[clap(long)]
json: bool,
/// Includes dapp canisters in the output.
#[clap(long)]
include_dapps: bool,
}

#[derive(Debug, Serialize, Deserialize)]
struct SnsLintInfo {
name: String,
memory_consumption: Vec<(u64, SnsCanisterType)>,
cycles: Vec<(u64, SnsCanisterType)>,
num_remaining_upgrade_steps: usize,
}

impl TableRow for SnsLintInfo {
fn column_names() -> Vec<&'static str> {
vec!["Name", "Memory", "Cycles", "Upgrades Remaining"]
}

fn column_values(&self) -> Vec<String> {
let high_memory_consumption = self
.memory_consumption
.iter()
.filter(|(memory_consumption, _)| {
(*memory_consumption as f64) > 2.5 * 1024.0 * 1024.0 * 1024.0
})
.map(|(memory_consumption, canister_type)| {
format!(
"{canister_type}: ({:.2} GiB)",
*memory_consumption as f64 / 1024.0 / 1024.0 / 1024.0
)
})
.join(", ");

let high_memory_consumption = if !high_memory_consumption.is_empty() {
format!("❌ {high_memory_consumption}")
} else {
"👍".to_string()
};

let low_cycles = self
.cycles
.iter()
.filter(|(cycles, _)| (*cycles as f64) < 10.0 * 1000.0 * 1000.0 * 1000.0 * 1000.0)
.map(|(cycles, canister_type)| {
format!(
"{canister_type}: ({:.2} TC)",
*cycles as f64 / 1000.0 / 1000.0 / 1000.0 / 1000.0
)
})
.join(", ");
let low_cycles = if !low_cycles.is_empty() {
format!("❌ {low_cycles}")
} else {
"👍".to_string()
};

vec![
self.name.clone(),
high_memory_consumption,
low_cycles,
format!("{}", self.num_remaining_upgrade_steps),
]
}
}

pub async fn exec(args: LintArgs, agent: &Agent) -> Result<()> {
eprintln!("Linting SNSes...");

let snses = sns_wasm::list_deployed_snses(agent).await?;
let num_total_snses = snses.len();
let snses_with_metadata = get_snses_with_metadata(agent, snses).await;

let num_snses_with_metadata = snses_with_metadata.len();

let lint_info: Vec<SnsLintInfo> = stream::iter(snses_with_metadata)
.map(|SnsWithMetadata { sns, name }| async move {
let summary = sns.root.sns_canisters_summary(agent).await?;
let statuses = summary
.into_iter()
.inspect(|(canister_summary, ctype)| {
if canister_summary.is_none() {
eprintln!("SNS {name} canister summary is missing {ctype}");
}
})
.filter_map(|(canister_summary, ctype)| {
canister_summary.map(|canister_summary| (canister_summary, ctype))
})
.inspect(|(canister_summary, ctype)| {
if canister_summary.status.is_none() {
eprintln!("SNS {name} canister {ctype} has no status");
}
})
.filter_map(|(canister_summary, ctype)| {
canister_summary
.status
.map(|canister_summary| (canister_summary, ctype))
})
.collect::<Vec<_>>();

let statuses = if args.include_dapps {
statuses
} else {
statuses
.into_iter()
.filter(|(_, ctype)| *ctype != SnsCanisterType::Dapp)
.collect()
};

let (memory_consumption, cycles) = statuses
.into_iter()
.map(|(canister_status, ctype)| {
(
(u64::try_from(canister_status.memory_size.0).unwrap(), ctype),
(u64::try_from(canister_status.cycles.0).unwrap(), ctype),
)
})
.unzip();

let num_remaining_upgrade_steps = sns
.remaining_upgrade_steps(agent)
.await?
.steps
.len()
.saturating_sub(1);

Result::<SnsLintInfo, anyhow::Error>::Ok(SnsLintInfo {
name,
memory_consumption,
cycles,
num_remaining_upgrade_steps,
})
})
.buffer_unordered(10)
.collect::<Vec<Result<_>>>()
.await
.into_iter()
.inspect(|result| {
if let Err(e) = result {
println!("Error: {}", e)
}
})
.filter_map(Result::ok)
.sorted_by(|a, b| a.name.cmp(&b.name))
.collect::<Vec<_>>();

let output = if args.json {
serde_json::to_string(&lint_info).unwrap()
} else {
as_table(lint_info.as_ref())
};
println!("{}", output);

eprintln!(
"Out of {num_total_snses} SNSes, {num_snses_with_metadata} had metadata and I linted {num_linted} of them.",
num_linted = lint_info.len()
);

Ok(())
}
32 changes: 5 additions & 27 deletions rs/sns/cli/src/list.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
use crate::table::{as_table, TableRow};
use crate::utils::{get_snses_with_metadata, SnsWithMetadata};
use anyhow::Result;
use clap::Parser;
use futures::{stream, StreamExt};
use ic_agent::Agent;
use ic_nervous_system_agent::{nns::sns_wasm, sns::Sns};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use ic_nervous_system_agent::nns::sns_wasm;

#[derive(Debug, Parser)]
pub struct ListArgs {
Expand All @@ -14,12 +12,6 @@ pub struct ListArgs {
json: bool,
}

#[derive(Debug, Serialize, Deserialize)]
struct SnsWithMetadata {
name: String,
sns: Sns,
}

impl TableRow for SnsWithMetadata {
fn column_names() -> Vec<&'static str> {
vec!["name", "ledger", "governance", "index", "swap", "root"]
Expand All @@ -38,24 +30,10 @@ impl TableRow for SnsWithMetadata {
}

pub async fn exec(args: ListArgs, agent: &Agent) -> Result<()> {
eprintln!("Listing SNSes...");

let snses = sns_wasm::list_deployed_snses(agent).await?;
let snses_with_metadata = stream::iter(snses)
.map(|sns| async move {
let metadata = sns.governance.metadata(agent).await?;
Ok((sns, metadata))
})
.buffer_unordered(10) // Do up to 10 requests at a time in parallel
.collect::<Vec<anyhow::Result<_>>>()
.await;
let snses_with_metadata = snses_with_metadata
.into_iter()
.filter_map(Result::ok)
.map(|(sns, metadata)| {
let name = metadata.name.unwrap_or_else(|| "Unknown".to_string());
SnsWithMetadata { name, sns }
})
.sorted_by(|a, b| a.name.cmp(&b.name))
.collect::<Vec<_>>();
let snses_with_metadata = get_snses_with_metadata(agent, snses).await;

let output = if args.json {
serde_json::to_string(&snses_with_metadata).unwrap()
Expand Down
Loading

0 comments on commit 5206cfc

Please sign in to comment.