diff --git a/rs/cli/src/commands/api_boundary_nodes/add.rs b/rs/cli/src/commands/api_boundary_nodes/add.rs index 2c7e1693e..cdda19e7c 100644 --- a/rs/cli/src/commands/api_boundary_nodes/add.rs +++ b/rs/cli/src/commands/api_boundary_nodes/add.rs @@ -39,6 +39,7 @@ impl ExecutableCommand for Add { title: Some(format!("Add {} API boundary node(s)", self.nodes.len())), summary: Some(format!("Add {} API boundary node(s)", self.nodes.len())), motivation: self.motivation.clone(), + forum_post_link: ctx.forum_post_link(), }, ) .await?; diff --git a/rs/cli/src/commands/api_boundary_nodes/remove.rs b/rs/cli/src/commands/api_boundary_nodes/remove.rs index ea2819795..2923b5d0f 100644 --- a/rs/cli/src/commands/api_boundary_nodes/remove.rs +++ b/rs/cli/src/commands/api_boundary_nodes/remove.rs @@ -31,6 +31,7 @@ impl ExecutableCommand for Remove { title: Some(format!("Remove {} API boundary node(s)", self.nodes.len())), summary: Some(format!("Remove {} API boundary node(s)", self.nodes.len())), motivation: self.motivation.clone(), + forum_post_link: ctx.forum_post_link(), }, ) .await?; diff --git a/rs/cli/src/commands/api_boundary_nodes/update.rs b/rs/cli/src/commands/api_boundary_nodes/update.rs index 4877213ee..18a443bd2 100644 --- a/rs/cli/src/commands/api_boundary_nodes/update.rs +++ b/rs/cli/src/commands/api_boundary_nodes/update.rs @@ -38,6 +38,7 @@ impl ExecutableCommand for Update { title: Some(format!("Update {} API boundary node(s) to {}", self.nodes.len(), &self.version)), summary: Some(format!("Update {} API boundary node(s) to {}", self.nodes.len(), &self.version)), motivation: self.motivation.clone(), + forum_post_link: ctx.forum_post_link(), }, ) .await?; diff --git a/rs/cli/src/commands/heal.rs b/rs/cli/src/commands/heal.rs index 1de16b741..c595fa28b 100644 --- a/rs/cli/src/commands/heal.rs +++ b/rs/cli/src/commands/heal.rs @@ -12,7 +12,7 @@ impl ExecutableCommand for Heal { async fn execute(&self, ctx: crate::ctx::DreContext) -> anyhow::Result<()> { let runner = ctx.runner().await; - runner.network_heal().await + runner.network_heal(ctx.forum_post_link()).await } fn validate(&self, _cmd: &mut clap::Command) {} diff --git a/rs/cli/src/commands/hostos/rollout.rs b/rs/cli/src/commands/hostos/rollout.rs index 88cb310b7..0ff3670c0 100644 --- a/rs/cli/src/commands/hostos/rollout.rs +++ b/rs/cli/src/commands/hostos/rollout.rs @@ -21,7 +21,9 @@ impl ExecutableCommand for Rollout { async fn execute(&self, ctx: crate::ctx::DreContext) -> anyhow::Result<()> { let runner = ctx.runner().await; - runner.hostos_rollout(self.nodes.clone(), &self.version, None).await + runner + .hostos_rollout(self.nodes.clone(), &self.version, None, ctx.forum_post_link()) + .await } fn validate(&self, _cmd: &mut clap::Command) {} diff --git a/rs/cli/src/commands/hostos/rollout_from_node_group.rs b/rs/cli/src/commands/hostos/rollout_from_node_group.rs index d697e32c4..7a15dc21a 100644 --- a/rs/cli/src/commands/hostos/rollout_from_node_group.rs +++ b/rs/cli/src/commands/hostos/rollout_from_node_group.rs @@ -87,7 +87,9 @@ impl ExecutableCommand for RolloutFromNodeGroup { .hostos_rollout_nodes(update_group, &self.version, &self.only, &self.exclude) .await? { - return runner.hostos_rollout(nodes_to_update, &self.version, Some(summary)).await; + return runner + .hostos_rollout(nodes_to_update, &self.version, Some(summary), ctx.forum_post_link()) + .await; } Ok(()) diff --git a/rs/cli/src/commands/mod.rs b/rs/cli/src/commands/mod.rs index 9f00e865d..f5751ef9e 100644 --- a/rs/cli/src/commands/mod.rs +++ b/rs/cli/src/commands/mod.rs @@ -111,6 +111,10 @@ The argument is mandatory for testnets, and is optional for mainnet and staging" /// Useful for when the nns is unreachable #[clap(long)] pub no_sync: bool, + + /// Link to the related forum post, where proposal details can be discussed + #[clap(long, global = true, visible_aliases = &["forum-link", "forum"])] + pub forum_post_link: Option, } macro_rules! impl_executable_command_for_enums { diff --git a/rs/cli/src/commands/nodes/remove.rs b/rs/cli/src/commands/nodes/remove.rs index e2ce177c9..8e049af4f 100644 --- a/rs/cli/src/commands/nodes/remove.rs +++ b/rs/cli/src/commands/nodes/remove.rs @@ -39,6 +39,7 @@ impl ExecutableCommand for Remove { extra_nodes_filter: self.extra_nodes_filter.clone(), exclude: Some(self.exclude.clone()), motivation: self.motivation.clone().unwrap_or_default(), + forum_post_link: ctx.forum_post_link(), }) .await } diff --git a/rs/cli/src/commands/subnet/create.rs b/rs/cli/src/commands/subnet/create.rs index f2c99b1e2..36c7922df 100644 --- a/rs/cli/src/commands/subnet/create.rs +++ b/rs/cli/src/commands/subnet/create.rs @@ -66,6 +66,7 @@ impl ExecutableCommand for Create { include: self.include.clone().into(), }, motivation.to_string(), + ctx.forum_post_link(), self.replica_version.clone(), self.other_args.to_owned(), self.help_other_args, diff --git a/rs/cli/src/commands/subnet/deploy.rs b/rs/cli/src/commands/subnet/deploy.rs index e3b58d6d3..0bba6c639 100644 --- a/rs/cli/src/commands/subnet/deploy.rs +++ b/rs/cli/src/commands/subnet/deploy.rs @@ -21,7 +21,7 @@ impl ExecutableCommand for Deploy { async fn execute(&self, ctx: crate::ctx::DreContext) -> anyhow::Result<()> { let runner = ctx.runner().await; - runner.deploy(&self.id, &self.version).await + runner.deploy(&self.id, &self.version, ctx.forum_post_link()).await } fn validate(&self, _cmd: &mut clap::Command) {} diff --git a/rs/cli/src/commands/subnet/replace.rs b/rs/cli/src/commands/subnet/replace.rs index 8890bbb02..678e58f07 100644 --- a/rs/cli/src/commands/subnet/replace.rs +++ b/rs/cli/src/commands/subnet/replace.rs @@ -72,7 +72,7 @@ impl ExecutableCommand for Replace { let runner = ctx.runner().await; - runner.propose_subnet_change(subnet_change_response).await + runner.propose_subnet_change(subnet_change_response, ctx.forum_post_link()).await } fn validate(&self, cmd: &mut clap::Command) { diff --git a/rs/cli/src/commands/subnet/rescue.rs b/rs/cli/src/commands/subnet/rescue.rs index 9c940b896..709710843 100644 --- a/rs/cli/src/commands/subnet/rescue.rs +++ b/rs/cli/src/commands/subnet/rescue.rs @@ -22,7 +22,7 @@ impl ExecutableCommand for Rescue { async fn execute(&self, ctx: crate::ctx::DreContext) -> anyhow::Result<()> { let runner = ctx.runner().await; - runner.subnet_rescue(&self.id, self.keep_nodes.clone()).await + runner.subnet_rescue(&self.id, self.keep_nodes.clone(), ctx.forum_post_link()).await } fn validate(&self, _cmd: &mut clap::Command) {} diff --git a/rs/cli/src/commands/subnet/resize.rs b/rs/cli/src/commands/subnet/resize.rs index b939c5dc9..c5de1e6c8 100644 --- a/rs/cli/src/commands/subnet/resize.rs +++ b/rs/cli/src/commands/subnet/resize.rs @@ -54,6 +54,7 @@ impl ExecutableCommand for Resize { include: self.include.clone().into(), }, self.motivation.clone(), + ctx.forum_post_link(), &runner.health_of_nodes().await?, ) .await diff --git a/rs/cli/src/commands/update_authorized_subnets.rs b/rs/cli/src/commands/update_authorized_subnets.rs index 47077493c..57fa8c4b7 100644 --- a/rs/cli/src/commands/update_authorized_subnets.rs +++ b/rs/cli/src/commands/update_authorized_subnets.rs @@ -103,6 +103,7 @@ impl ExecutableCommand for UpdateAuthorizedSubnets { title: Some("Update list of public subnets".to_string()), summary: Some(summary), motivation: None, + forum_post_link: ctx.forum_post_link(), }, ) .await?; diff --git a/rs/cli/src/commands/update_unassigned_nodes.rs b/rs/cli/src/commands/update_unassigned_nodes.rs index 9880c59d0..e32945314 100644 --- a/rs/cli/src/commands/update_unassigned_nodes.rs +++ b/rs/cli/src/commands/update_unassigned_nodes.rs @@ -41,7 +41,9 @@ impl ExecutableCommand for UpdateUnassignedNodes { } }; - runner.update_unassigned_nodes(&PrincipalId::from_str(&nns_subnet_id)?).await + runner + .update_unassigned_nodes(&PrincipalId::from_str(&nns_subnet_id)?, ctx.forum_post_link()) + .await } fn validate(&self, _cmd: &mut clap::Command) {} diff --git a/rs/cli/src/commands/version/revise/guest_os.rs b/rs/cli/src/commands/version/revise/guest_os.rs index 8b6a03ee1..80bfda795 100644 --- a/rs/cli/src/commands/version/revise/guest_os.rs +++ b/rs/cli/src/commands/version/revise/guest_os.rs @@ -25,7 +25,13 @@ impl ExecutableCommand for GuestOs { async fn execute(&self, ctx: crate::ctx::DreContext) -> anyhow::Result<()> { let runner = ctx.runner().await; runner - .do_revise_elected_replica_versions(&ic_management_types::Artifact::GuestOs, &self.version, &self.release_tag, self.force) + .do_revise_elected_replica_versions( + &ic_management_types::Artifact::GuestOs, + &self.version, + &self.release_tag, + self.force, + ctx.forum_post_link(), + ) .await } diff --git a/rs/cli/src/commands/version/revise/host_os.rs b/rs/cli/src/commands/version/revise/host_os.rs index ee9f07452..045937645 100644 --- a/rs/cli/src/commands/version/revise/host_os.rs +++ b/rs/cli/src/commands/version/revise/host_os.rs @@ -25,7 +25,13 @@ impl ExecutableCommand for HostOs { async fn execute(&self, ctx: crate::ctx::DreContext) -> anyhow::Result<()> { let runner = ctx.runner().await; runner - .do_revise_elected_replica_versions(&ic_management_types::Artifact::HostOs, &self.version, &self.release_tag, self.force) + .do_revise_elected_replica_versions( + &ic_management_types::Artifact::HostOs, + &self.version, + &self.release_tag, + self.force, + ctx.forum_post_link(), + ) .await } diff --git a/rs/cli/src/ctx.rs b/rs/cli/src/ctx.rs index 5d6d091c9..7a6573d68 100644 --- a/rs/cli/src/ctx.rs +++ b/rs/cli/src/ctx.rs @@ -35,6 +35,7 @@ pub struct DreContext { verbose_runner: bool, skip_sync: bool, ic_admin_path: Option, + forum_post_link: Option, } impl DreContext { @@ -83,6 +84,7 @@ impl DreContext { verbose_runner: args.verbose, skip_sync: args.no_sync, ic_admin_path, + forum_post_link: args.forum_post_link.clone(), }) } @@ -234,4 +236,8 @@ impl DreContext { *self.runner.borrow_mut() = Some(runner.clone()); runner } + + pub fn forum_post_link(&self) -> Option { + self.forum_post_link.clone() + } } diff --git a/rs/cli/src/ic_admin.rs b/rs/cli/src/ic_admin.rs index e3bc7b4e6..e13d9ffa4 100644 --- a/rs/cli/src/ic_admin.rs +++ b/rs/cli/src/ic_admin.rs @@ -143,7 +143,19 @@ impl IcAdminWrapper { .map(|s| { vec![ "--summary".to_string(), - format!("{}{}", s, opts.motivation.map(|m| format!("\n\nMotivation: {m}")).unwrap_or_default(),), + format!( + "{}{}", + s, + opts.motivation + .map(|m| format!( + "\n\nMotivation: {m}{}", + match opts.forum_post_link { + Some(link) => format!("\nForum post link: {}\n", link), + None => "".to_string(), + } + )) + .unwrap_or_default(), + ), ] }) .unwrap_or_default(), @@ -720,6 +732,7 @@ pub struct ProposeOptions { pub title: Option, pub summary: Option, pub motivation: Option, + pub forum_post_link: Option, } const DEFAULT_IC_ADMIN_VERSION: &str = "26d5f9d0bdca0a817c236134dc9c7317b32c69a5"; diff --git a/rs/cli/src/qualification/retire_blessed_versions.rs b/rs/cli/src/qualification/retire_blessed_versions.rs index d0314191f..9637c80da 100644 --- a/rs/cli/src/qualification/retire_blessed_versions.rs +++ b/rs/cli/src/qualification/retire_blessed_versions.rs @@ -49,6 +49,7 @@ impl Step for RetireBlessedVersions { title: Some("Retire replica versions".to_string()), summary: Some("Unelecting a version".to_string()), motivation: Some("Unelecting a version".to_string()), + forum_post_link: None, }, ) .await diff --git a/rs/cli/src/qualification/upgrade_subnets.rs b/rs/cli/src/qualification/upgrade_subnets.rs index f8ce60750..a4bb49634 100644 --- a/rs/cli/src/qualification/upgrade_subnets.rs +++ b/rs/cli/src/qualification/upgrade_subnets.rs @@ -99,6 +99,7 @@ impl Step for UpgradeSubnets { title: Some(format!("Propose to upgrade subnet {} to {}", subnet.principal, &self.to_version)), summary: Some("Qualification testing".to_string()), motivation: Some("Qualification testing".to_string()), + forum_post_link: None, }, ) .await @@ -140,6 +141,7 @@ impl Step for UpgradeSubnets { title: Some("Upgrading unassigned nodes".to_string()), summary: Some("Upgrading unassigned nodes".to_string()), motivation: Some("Upgrading unassigned nodes".to_string()), + forum_post_link: None, }, ) .await diff --git a/rs/cli/src/runner.rs b/rs/cli/src/runner.rs index 0d40de293..a23f12aa6 100644 --- a/rs/cli/src/runner.rs +++ b/rs/cli/src/runner.rs @@ -89,7 +89,7 @@ impl Runner { ic_repo } - pub async fn deploy(&self, subnet: &PrincipalId, version: &str) -> anyhow::Result<()> { + pub async fn deploy(&self, subnet: &PrincipalId, version: &str, forum_post_link: Option) -> anyhow::Result<()> { let _ = self .ic_admin .propose_run( @@ -101,6 +101,7 @@ impl Runner { title: format!("Update subnet {subnet} to GuestOS version {version}").into(), summary: format!("Update subnet {subnet} to GuestOS version {version}").into(), motivation: None, + forum_post_link, }, ) .await?; @@ -117,6 +118,7 @@ impl Runner { &self, request: ic_management_types::requests::SubnetResizeRequest, motivation: String, + forum_post_link: Option, health_of_nodes: &BTreeMap, ) -> anyhow::Result<()> { let change = self @@ -140,7 +142,8 @@ impl Runner { return Ok(()); } if change.added_with_desc.len() == change.removed_with_desc.len() { - self.run_membership_change(change.clone(), replace_proposal_options(&change)?).await + self.run_membership_change(change.clone(), replace_proposal_options(&change, forum_post_link)?) + .await } else { let action = if change.added_with_desc.len() < change.removed_with_desc.len() { "Removing nodes from" @@ -153,6 +156,7 @@ impl Runner { title: format!("{action} subnet {}", request.subnet).into(), summary: format!("{action} subnet {}", request.subnet).into(), motivation: motivation.clone().into(), + forum_post_link, }, ) .await @@ -162,6 +166,7 @@ impl Runner { &self, request: ic_management_types::requests::SubnetCreateRequest, motivation: String, + forum_post_link: Option, replica_version: Option, other_args: Vec, help_other_args: bool, @@ -212,13 +217,14 @@ impl Runner { title: Some("Creating new subnet".into()), summary: Some("# Creating new subnet with nodes: ".into()), motivation: Some(motivation.clone()), + forum_post_link, }, ) .await?; Ok(()) } - pub async fn propose_subnet_change(&self, change: SubnetChangeResponse) -> anyhow::Result<()> { + pub async fn propose_subnet_change(&self, change: SubnetChangeResponse, forum_post_link: Option) -> anyhow::Result<()> { if self.verbose { if let Some(run_log) = &change.run_log { println!("{}\n", run_log.join("\n")); @@ -229,7 +235,7 @@ impl Runner { return Ok(()); } - let options = replace_proposal_options(&change)?; + let options = replace_proposal_options(&change, forum_post_link)?; self.run_membership_change(change, options).await } @@ -278,6 +284,7 @@ impl Runner { version: &String, release_tag: &String, force: bool, + forum_post_link: Option, ) -> anyhow::Result<()> { let update_version = IcAdminWrapper::prepare_to_propose_to_revise_elected_versions( release_artifact, @@ -298,6 +305,7 @@ impl Runner { title: Some(update_version.title), summary: Some(update_version.summary.clone()), motivation: None, + forum_post_link, }, ) .await?; @@ -395,7 +403,13 @@ impl Runner { } } - pub async fn hostos_rollout(&self, nodes: Vec, version: &str, maybe_summary: Option) -> anyhow::Result<()> { + pub async fn hostos_rollout( + &self, + nodes: Vec, + version: &str, + maybe_summary: Option, + forum_post_link: Option, + ) -> anyhow::Result<()> { let title = format!("Set HostOS version: {version} on {} nodes", nodes.clone().len()); self.ic_admin @@ -408,6 +422,7 @@ impl Runner { title: title.clone().into(), summary: maybe_summary.unwrap_or(title).into(), motivation: None, + forum_post_link, }, ) .await @@ -469,13 +484,14 @@ impl Runner { title: "Remove nodes from the network".to_string().into(), summary: "Remove nodes from the network".to_string().into(), motivation: motivation.into(), + forum_post_link: nodes_remover.forum_post_link, }, ) .await?; Ok(()) } - pub async fn network_heal(&self) -> anyhow::Result<()> { + pub async fn network_heal(&self, forum_post_link: Option) -> anyhow::Result<()> { let health_client = health::HealthClient::new(self.network.clone()); let mut errors = vec![]; @@ -501,7 +517,7 @@ impl Runner { for change in &subnets_change_response { let _ = self - .run_membership_change(change.clone(), replace_proposal_options(change)?) + .run_membership_change(change.clone(), replace_proposal_options(change, forum_post_link.clone())?) .await .map_err(|e| { println!("{}", e); @@ -562,7 +578,7 @@ impl Runner { Ok(()) } - pub async fn subnet_rescue(&self, subnet: &PrincipalId, keep_nodes: Option>) -> anyhow::Result<()> { + pub async fn subnet_rescue(&self, subnet: &PrincipalId, keep_nodes: Option>, forum_post_link: Option) -> anyhow::Result<()> { let change_request = self .registry .modify_subnet_nodes(SubnetQueryBy::SubnetId(*subnet)) @@ -582,7 +598,8 @@ impl Runner { return Ok(()); } - self.run_membership_change(change.clone(), replace_proposal_options(&change)?).await + self.run_membership_change(change.clone(), replace_proposal_options(&change, forum_post_link)?) + .await } pub async fn retireable_versions(&self, artifact: &Artifact) -> anyhow::Result> { @@ -679,7 +696,7 @@ impl Runner { Ok(()) } - pub async fn update_unassigned_nodes(&self, nns_subnet_id: &PrincipalId) -> anyhow::Result<()> { + pub async fn update_unassigned_nodes(&self, nns_subnet_id: &PrincipalId, forum_post_link: Option) -> anyhow::Result<()> { let subnets = self.registry.subnets().await?; let nns = match subnets.get_key_value(nns_subnet_id) { @@ -709,6 +726,7 @@ impl Runner { summary: Some("Update the unassigned nodes to the latest rolled-out version".to_string()), motivation: None, title: Some("Update all unassigned nodes".to_string()), + forum_post_link, }; self.ic_admin.propose_run(command, options).await?; @@ -716,7 +734,7 @@ impl Runner { } } -pub fn replace_proposal_options(change: &SubnetChangeResponse) -> anyhow::Result { +pub fn replace_proposal_options(change: &SubnetChangeResponse, forum_post_link: Option) -> anyhow::Result { let subnet_id = change.subnet_id.ok_or_else(|| anyhow::anyhow!("subnet_id is required"))?.to_string(); let replace_target = if change.added_with_desc.len() > 1 || change.removed_with_desc.len() > 1 { @@ -730,6 +748,7 @@ pub fn replace_proposal_options(change: &SubnetChangeResponse) -> anyhow::Result title: format!("Replace {replace_target} in subnet {subnet_id_short}",).into(), summary: format!("# Replace {replace_target} in subnet {subnet_id_short}",).into(), motivation: Some(format!("{}\n\n{}\n", change.motivation.as_ref().unwrap_or(&String::new()), change)), + forum_post_link, }) } diff --git a/rs/cli/src/subnet_manager.rs b/rs/cli/src/subnet_manager.rs index 46726b095..7a76eaa56 100644 --- a/rs/cli/src/subnet_manager.rs +++ b/rs/cli/src/subnet_manager.rs @@ -161,9 +161,12 @@ impl SubnetManager { for (n, _) in change.removed().iter().filter(|(n, _)| !node_ids_unhealthy.contains(&n.id)) { motivations.push(format!( - "replacing {} as per user request: {}", + "replacing {} as per user request{}", n.id, - motivation.clone().unwrap_or("as per user request".to_string()) + match motivation { + Some(ref m) => format!(": {}", m), + None => "".to_string(), + } )); } diff --git a/rs/decentralization/src/subnets.rs b/rs/decentralization/src/subnets.rs index c0aef9bc2..83d90ed7c 100644 --- a/rs/decentralization/src/subnets.rs +++ b/rs/decentralization/src/subnets.rs @@ -40,6 +40,7 @@ pub struct NodesRemover { pub extra_nodes_filter: Vec, pub exclude: Option>, pub motivation: String, + pub forum_post_link: Option, } impl NodesRemover { pub fn remove_nodes( diff --git a/rs/qualifier/src/qualify_util.rs b/rs/qualifier/src/qualify_util.rs index 9ca9379d4..7aa26b6e5 100644 --- a/rs/qualifier/src/qualify_util.rs +++ b/rs/qualifier/src/qualify_util.rs @@ -86,6 +86,7 @@ pub async fn qualify( }), verbose: false, no_sync: false, + forum_post_link: None, }; let ctx = DreContext::from_args(&args).await?;