Skip to content
Snippets Groups Projects
Commit c8659d9d authored by Pascal Engélibert's avatar Pascal Engélibert :bicyclist:
Browse files

Command notify

parent 020be31e
No related branches found
No related tags found
1 merge request!10Command notify
{
"rust-analyzer.showUnlinkedFileNotification": false
}
\ No newline at end of file
This diff is collapsed.
...@@ -8,12 +8,15 @@ version = "0.1.0" ...@@ -8,12 +8,15 @@ version = "0.1.0"
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
chrono = "0.4.26"
clap = { version = "4.1.9", features = ["derive"] } clap = { version = "4.1.9", features = ["derive"] }
codec = { package = "parity-scale-codec", version = "3.4.0" } codec = { package = "parity-scale-codec", version = "3.4.0" }
confy = "0.5.1"
env_logger = "0.10" env_logger = "0.10"
futures = "0.3.27" futures = "0.3.27"
graphql_client = { version = "0.12.0", features = ["reqwest"] } graphql_client = { version = "0.12.0", features = ["reqwest"] }
hex = "0.4.3" hex = "0.4.3"
lettre = "0.10.4"
log = "0.4.17" log = "0.4.17"
reqwest = "0.11.14" reqwest = "0.11.14"
rpassword = "7.2.0" rpassword = "7.2.0"
...@@ -22,11 +25,11 @@ serde_json = "1.0.94" ...@@ -22,11 +25,11 @@ serde_json = "1.0.94"
sp-core = { git = "https://github.com/duniter/substrate", branch = "duniter-substrate-v0.9.32" } sp-core = { git = "https://github.com/duniter/substrate", branch = "duniter-substrate-v0.9.32" }
subxt = { git = "https://github.com/duniter/subxt.git", branch = "duniter-substrate-v0.9.32" } subxt = { git = "https://github.com/duniter/subxt.git", branch = "duniter-substrate-v0.9.32" }
tokio = { version = "1.26.0", features = ["macros"] } tokio = { version = "1.26.0", features = ["macros"] }
confy = "0.5.1"
# allows to build gcli for different runtimes and with different predefined networks # allows to build gcli for different runtimes and with different predefined networks
[features] [features]
default = ["gdev"] # default runtime is "gdev", gdev network is available default = ["gdev"] # default runtime is "gdev", gdev network is available
gdev = [] gdev = []
gtest = [] gtest = []
g1 = [] g1 = []
use crate::indexer::*; use crate::{indexer::*, *};
use crate::*;
use std::collections::{hash_map, HashMap}; use std::collections::{hash_map, HashMap};
......
...@@ -4,6 +4,7 @@ pub mod collective; ...@@ -4,6 +4,7 @@ pub mod collective;
pub mod expire; pub mod expire;
pub mod identity; pub mod identity;
pub mod net_test; pub mod net_test;
pub mod notify;
pub mod oneshot; pub mod oneshot;
pub mod revocation; pub mod revocation;
pub mod runtime; pub mod runtime;
......
use crate::*; use crate::*;
use crate::commands::revocation::generate_revoc_doc; use crate::{
use crate::runtime::runtime_types::common_runtime::entities::IdtyData; commands::revocation::generate_revoc_doc,
use crate::runtime::runtime_types::pallet_identity::types::*; runtime::runtime_types::{
use crate::runtime::runtime_types::sp_core::sr25519::Signature; common_runtime::entities::IdtyData, pallet_identity::types::*, sp_core::sr25519::Signature,
use crate::runtime::runtime_types::sp_runtime::MultiSignature; sp_runtime::MultiSignature,
},
};
use std::str::FromStr; use std::str::FromStr;
/// define identity subcommands /// define identity subcommands
...@@ -42,18 +44,12 @@ pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<( ...@@ -42,18 +44,12 @@ pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<(
match command { match command {
Subcommand::Show => {} Subcommand::Show => {}
Subcommand::Get { Subcommand::Get {
ref account_id, account_id,
identity_id, identity_id,
ref username, username,
} => { } => {
data = data.build_indexer().await?; data = data.build_indexer().await?;
commands::identity::get_identity( commands::identity::get_identity(&data, account_id, identity_id, username).await?
&data,
account_id.clone(),
identity_id,
username.clone(),
)
.await?
} }
Subcommand::Create { target } => { Subcommand::Create { target } => {
commands::identity::create_identity(data.keypair(), data.client(), target).await?; commands::identity::create_identity(data.keypair(), data.client(), target).await?;
......
use crate::*;
use futures::join;
use lettre::{
message::header::ContentType, transport::smtp::authentication::Credentials, Message,
SmtpTransport, Transport,
};
/// (ms)
const BLOCK_DURATION: f32 = 6_000.0;
/// (ms)
const SESSION_DURATION: f32 = 3_600_000.0;
fn millis_to_3339(millis: i64) -> String {
chrono::DateTime::<chrono::Utc>::from_utc(
chrono::NaiveDateTime::from_timestamp_millis(millis).unwrap(),
chrono::Utc,
)
.to_rfc3339()
}
#[derive(Clone, Debug, clap::Parser)]
pub enum Subcommand {
/// Send email for upcoming expirations that require an action, for a given identity (if needed)
NotifyExpire {
/// SMTP username
#[clap(short = 'u', long)]
smtp_username: String,
/// SMTP password
#[clap(short = 'p', long, default_value = "")]
smtp_password: String,
/// SMTP hostname
#[clap(short, long, default_value = "127.0.0.1")]
smtp: String,
/// Notify expirations happening before this number of days
#[clap(short, long, default_value_t = 10.0)]
before: f32,
/// Email To
#[clap(short, long, default_value = "root <root@localhost>")]
to: String,
/// Email From
#[clap(short, long, default_value = "gcli <noreply@localhost>")]
from: String,
/// Minimum number of received certs
#[clap(long, default_value_t = 5)]
min_certs: usize,
/// Minimum number of received smith certs
#[clap(long, default_value_t = 5)]
min_smith_certs: usize,
},
/// List upcoming expirations that require an action, for all identities
ShowExpire {
/// Show certs that expire within less than this number of blocks
#[clap(short, long, default_value_t = 100800)]
blocks: u32,
/// Show authorities that should rotate keys within less than this number of sessions
#[clap(short, long, default_value_t = 100)]
sessions: u32,
},
}
/// handle monitoring commands
pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<()> {
let mut data = data.build_client().await?;
match command {
Subcommand::ShowExpire { blocks, sessions } => {
data = data.build_client().await?.build_indexer().await?;
commands::expire::monitor_expirations(&data, blocks, sessions).await?
}
Subcommand::NotifyExpire {
smtp_username,
smtp_password,
smtp,
before,
to,
from,
min_certs,
min_smith_certs,
} => {
data = data.build_client().await?.fetch_idty_index().await?;
notify_expirations(
&data,
before,
smtp_username,
smtp_password,
smtp,
to,
from,
min_certs,
min_smith_certs,
)
.await?
}
};
Ok(())
}
pub async fn notify_expirations(
data: &Data,
before: f32,
smtp_username: String,
smtp_password: String,
smtp: String,
to: String,
from: String,
min_certs: usize,
min_smith_certs: usize,
) -> anyhow::Result<()> {
let client = data.client();
let idty = data.idty_index();
let parent_hash = client
.storage()
.fetch(&runtime::storage().system().parent_hash(), None)
.await?
.unwrap();
let addr_current_block = runtime::storage().system().number();
let addr_current_session = runtime::storage().session().current_index();
let addr_now = runtime::storage().timestamp().now();
let addr_membership = runtime::storage().membership().membership(idty);
let addr_authority_membership = runtime::storage().authority_members().members(idty);
let addr_cert = runtime::storage().cert().certs_by_receiver(idty);
let addr_smith_cert = runtime::storage().smith_cert().certs_by_receiver(idty);
let (current_block, current_session, now, membership, authority_membership, certs, smith_certs) = join!(
client
.storage()
.fetch(&addr_current_block, Some(parent_hash)),
client
.storage()
.fetch(&addr_current_session, Some(parent_hash)),
client.storage().fetch(&addr_now, Some(parent_hash)),
client.storage().fetch(&addr_membership, Some(parent_hash)),
client
.storage()
.fetch(&addr_authority_membership, Some(parent_hash)),
client.storage().fetch(&addr_cert, Some(parent_hash)),
client.storage().fetch(&addr_smith_cert, Some(parent_hash)),
);
let current_block = current_block?.unwrap_or_default();
let current_session = current_session?.unwrap_or_default();
let now = now?.unwrap_or_default();
let membership_expire = membership?.map(|membership| membership.expire_on);
let authority_membership = authority_membership?.map(|authority_membership| {
(
authority_membership.expire_on_session,
authority_membership.must_rotate_keys_before,
)
});
let mut certs: Vec<_> = certs?.unwrap_or_default();
let mut smith_certs: Vec<_> = smith_certs?.unwrap_or_default();
let end_block = current_block + (before * 86_400_000.0 / BLOCK_DURATION) as u32;
let end_session = current_session + (before * 86_400_000.0 / SESSION_DURATION) as u32;
let not_enough_certs_at = find_cert_expirations(&mut certs, end_block, min_certs);
let not_enough_smith_certs_at =
find_cert_expirations(&mut smith_certs, end_block, min_smith_certs);
let mut body = format!(
"Gcli detected some expirations about {}:\r\n\r\n",
data.address()
);
let mut send = false;
if let Some(membership_expire) = membership_expire {
if membership_expire <= end_block && membership_expire >= current_block {
body.push_str(&format!(
"Membership will expire at {} (block {})\r\n\r\n",
millis_to_3339(
now as i64 + (membership_expire - current_block) as i64 * BLOCK_DURATION as i64
),
membership_expire
));
send = true;
}
}
if let Some((authority_membership_expire, session_keys_expire)) = authority_membership {
if authority_membership_expire <= end_session
&& authority_membership_expire >= current_session
{
body.push_str(&format!(
"Authority membership will expire at {} (session {})\r\n\r\n",
millis_to_3339(
now as i64
+ (authority_membership_expire - current_session) as i64
* SESSION_DURATION as i64
),
authority_membership_expire
));
send = true;
}
if session_keys_expire <= end_session && session_keys_expire >= current_session {
body.push_str(&format!(
"Session keys will expire at {} (session {})\r\n\r\n",
millis_to_3339(
now as i64
+ (session_keys_expire - current_session) as i64 * SESSION_DURATION as i64
),
session_keys_expire
));
send = true;
}
}
if let Some(not_enough_certs_at) = not_enough_certs_at {
body.push_str(&format!(
"Not enough certifications at {} (block {}):\r\n",
millis_to_3339(
now as i64 + (not_enough_certs_at - current_block) as i64 * BLOCK_DURATION as i64
),
not_enough_certs_at
));
for (issuer, cert_expire) in certs.iter() {
if *cert_expire > end_block {
break;
}
body.push_str(&format!(
" * {} will expire at {} (block {})\r\n",
issuer,
millis_to_3339(
now as i64 + (cert_expire - current_block) as i64 * BLOCK_DURATION as i64
),
*cert_expire
));
}
body.push_str("\r\n");
send = true;
}
if let Some(not_enough_certs_at) = not_enough_smith_certs_at {
body.push_str(&format!(
"Not enough smith certifications at {} (block {}):\r\n",
millis_to_3339(
now as i64 + (not_enough_certs_at - current_block) as i64 * BLOCK_DURATION as i64
),
not_enough_certs_at
));
for (issuer, cert_expire) in certs.iter() {
if *cert_expire > end_block {
break;
}
body.push_str(&format!(
" * {} will expire at {} (block {})\r\n",
issuer,
millis_to_3339(
now as i64 + (*cert_expire - current_block) as i64 * BLOCK_DURATION as i64
),
cert_expire
));
}
body.push_str("\r\n");
send = true;
}
if send {
let email = Message::builder()
.from(from.parse().unwrap())
.to(to.parse().unwrap())
.subject(format!(
"[Gcli] Upcoming expirations for {}",
data.address()
))
.header(ContentType::TEXT_PLAIN)
.body(body)
.unwrap();
let creds = Credentials::new(smtp_username, smtp_password);
// Open a remote connection to gmail
let mailer = SmtpTransport::relay(&smtp)
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
} else {
println!("OK, nothing to send.");
}
Ok(())
}
/// Returns the block number when there will not be enough certs
/// Side effect: Sorts certs by ascending expiration block
fn find_cert_expirations(
certs: &mut [(u32, u32)],
end_block: u32,
min_certs: usize,
) -> Option<u32> {
if min_certs == 0 || certs.is_empty() {
return None;
}
// Sort certs by ascending expiration block
certs.sort_unstable_by_key(|(_idty, expire)| *expire);
// Are there at least `min_certs` certs that will expire after `end_block`?
let not_enough_at = certs[certs.len().saturating_sub(min_certs)].1;
if not_enough_at <= end_block {
Some(not_enough_at)
} else {
None
}
}
...@@ -23,15 +23,6 @@ pub enum Subcommand { ...@@ -23,15 +23,6 @@ pub enum Subcommand {
UpdateKeys, UpdateKeys,
/// set sudo keys /// set sudo keys
SudoSetKey { new_key: AccountId }, SudoSetKey { new_key: AccountId },
/// List upcoming expirations that require an action
ShowExpire {
/// Show certs that expire within less than this number of blocks
#[clap(short, long, default_value_t = 100800)]
blocks: u32,
/// Show authorities that should rotate keys within less than this number of sessions
#[clap(short, long, default_value_t = 100)]
sessions: u32,
},
/// List online authorities /// List online authorities
ShowOnline, ShowOnline,
} }
...@@ -60,10 +51,6 @@ pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<( ...@@ -60,10 +51,6 @@ pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<(
data = data.build_client().await?; data = data.build_client().await?;
commands::sudo::set_key(data.keypair(), data.client(), new_key).await?; commands::sudo::set_key(data.keypair(), data.client(), new_key).await?;
} }
Subcommand::ShowExpire { blocks, sessions } => {
data = data.build_client().await?.build_indexer().await?;
commands::expire::monitor_expirations(&data, blocks, sessions).await?
}
Subcommand::ShowOnline => { Subcommand::ShowOnline => {
data = data.build_client().await?; data = data.build_client().await?;
online(&data).await? online(&data).await?
...@@ -79,7 +66,13 @@ pub async fn rotate_keys(client: &Client) -> Result<SessionKeys, anyhow::Error> ...@@ -79,7 +66,13 @@ pub async fn rotate_keys(client: &Client) -> Result<SessionKeys, anyhow::Error>
client client
.rpc() .rpc()
.rotate_keys() .rotate_keys()
.await.map_err(|e| anyhow!("Please make sure you are connected to your validator node with the unsafe RPC API enabled {e}"))? .await
.map_err(|e| {
anyhow!(
"Please make sure you are connected to your validator node with the unsafe RPC \
API enabled {e}"
)
})?
.deref() .deref()
.try_into() .try_into()
.map_err(|e| anyhow!("Session keys have wrong length: {:?}", e)) .map_err(|e| anyhow!("Session keys have wrong length: {:?}", e))
......
...@@ -158,7 +158,7 @@ impl Data { ...@@ -158,7 +158,7 @@ impl Data {
"test1" => "2", "test1" => "2",
"test2" => "4", "test2" => "4",
"test3" => "3", "test3" => "3",
_ => "" _ => "",
}; };
self.cfg.secret = Some(format!("{TEST_MNEMONIC}//{derivation}")); self.cfg.secret = Some(format!("{TEST_MNEMONIC}//{derivation}"));
} else { } else {
......
use graphql_client::reqwest::post_graphql; use graphql_client::{reqwest::post_graphql, GraphQLQuery};
use graphql_client::GraphQLQuery;
use crate::*; use crate::*;
......
...@@ -15,8 +15,10 @@ use keys::*; ...@@ -15,8 +15,10 @@ use keys::*;
use runtime_config::*; use runtime_config::*;
use serde::Deserialize; use serde::Deserialize;
use sp_core::{sr25519::Pair, Pair as _}; use sp_core::{sr25519::Pair, Pair as _};
use subxt::blocks::ExtrinsicEvents; use subxt::{
use subxt::tx::{BaseExtrinsicParamsBuilder, PairSigner, TxStatus}; blocks::ExtrinsicEvents,
tx::{BaseExtrinsicParamsBuilder, PairSigner, TxStatus},
};
use utils::*; use utils::*;
/// define command line arguments /// define command line arguments
...@@ -81,6 +83,9 @@ pub enum Subcommand { ...@@ -81,6 +83,9 @@ pub enum Subcommand {
/// Indexer (check, latest block) /// Indexer (check, latest block)
#[clap(subcommand)] #[clap(subcommand)]
Indexer(indexer::Subcommand), Indexer(indexer::Subcommand),
/// Monitor (expire, notify)
#[clap(subcommand)]
Monitor(commands::notify::Subcommand),
/// Config (show, save...) /// Config (show, save...)
#[clap(subcommand)] #[clap(subcommand)]
Config(conf::Subcommand), Config(conf::Subcommand),
...@@ -97,7 +102,7 @@ async fn main() -> Result<(), GcliError> { ...@@ -97,7 +102,7 @@ async fn main() -> Result<(), GcliError> {
// match subcommands // match subcommands
match data.args.subcommand.clone() { match data.args.subcommand.clone() {
Subcommand::DoNothing => {Ok(())} Subcommand::DoNothing => Ok(()),
Subcommand::Account(subcommand) => { Subcommand::Account(subcommand) => {
commands::account::handle_command(data, subcommand).await commands::account::handle_command(data, subcommand).await
} }
...@@ -116,6 +121,8 @@ async fn main() -> Result<(), GcliError> { ...@@ -116,6 +121,8 @@ async fn main() -> Result<(), GcliError> {
commands::blockchain::handle_command(data, subcommand).await commands::blockchain::handle_command(data, subcommand).await
} }
Subcommand::Indexer(subcommand) => indexer::handle_command(data, subcommand).await, Subcommand::Indexer(subcommand) => indexer::handle_command(data, subcommand).await,
Subcommand::Monitor(subcommand) => commands::notify::handle_command(data, subcommand).await,
Subcommand::Config(subcommand) => conf::handle_command(data, subcommand), Subcommand::Config(subcommand) => conf::handle_command(data, subcommand),
}.map_err(|e| dbg!(e).into()) }
.map_err(|e| dbg!(e).into())
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment