Files
nym/ephemera/src/config/mod.rs
T
2023-11-21 09:02:48 +01:00

335 lines
13 KiB
Rust

//! Configuration for Ephemeris node. It contains mandatory settings for a node to start.
//!
//! Default location for the configuration file is `~/.nym/ephemera/ephemera.toml`.
//! Or relative to a node specific directory `~/.nym/ephemera/<node_name>/ephemera.toml`.
use std::io::Write;
use std::path::PathBuf;
use config::ConfigError;
use log::{error, info};
use nym_config::{DEFAULT_NYM_APIS_DIR, NYM_DIR};
use serde_derive::{Deserialize, Serialize};
use thiserror::Error;
//TODO - validate configuration at load time
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Configuration {
/// Configuration related to node instance identity
pub node: NodeConfiguration,
/// Configuration for libp2p network
pub libp2p: Libp2pConfiguration,
/// Configuration for Ephemera embedded database
pub storage: DatabaseConfiguration,
/// Configuration for websocket
pub websocket: WebsocketConfiguration,
/// Configuration for embedded http API server
pub http: HttpConfiguration,
/// Configuration related to block creation
pub block_manager: BlockManagerConfiguration,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct NodeConfiguration {
/// Node IP shared by all Ephemera services like libp2p, websocket, http.
/// If separate IP/DNS is needed for each service, it should be configured outside of Ephemera.
pub ip: String,
//FIXME: dev only
/// If private key is stored in configuration as plain text, read it from here.
/// Private key is mandatory for a node to be able to function in the network.
/// It is used to signe protocol messages and identify node in the network.
pub private_key: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum MembershipKind {
/// Mandatory minimum membership size is defined by threshold of all peers returned by membership provider.
/// Threshold value is defined the ratio of peers that need to be available.
/// For example, if the threshold is 0.5, then at least 50% of the peers need to be available.
Threshold,
/// Mandatory minimum membership size is all peers who are online.
AnyOnline,
/// Mandatory minimum membership size is all peers returned by membership provider.
AllOnline,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Libp2pConfiguration {
/// Port to listen on for libp2p internal connections
pub port: u16,
/// Gossipsub topic to gossip Ephemera messages between peers. Ephemera listens messages
/// only from this topic. Invalid topic configuration means that Ephemera is not able to
/// reach messages from other peers.
pub ephemera_msg_topic_name: String,
/// Gossipsub interval to check its mesh health.
pub heartbeat_interval_sec: u64,
/// How often Ephemera checks its membership rendezvous endpoint. It's configurable with second granularity.
/// But in general it's up to the user and depends how rendezvous endpoint is configured.
///
/// Ephemera uses rendezvous endpoint as authority to tell which nodes are authorized to participate.
/// So it should be configured and implemented in a manner that nodes always have the most up to date and
/// accurate information.
pub members_provider_delay_sec: u64,
/// Defines how the actual membership is decided. See `[ephemera:]` for more details.
pub membership_kind: MembershipKind,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct DatabaseConfiguration {
/// Path to the RocksDb database directory
pub rocksdb_path: String,
/// Path to the SQLite database file
pub sqlite_path: String,
/// If to create database if it does not exist
pub create_if_not_exists: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct WebsocketConfiguration {
/// Port to listen on for WebSocket subscriptions.
pub port: u16,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct HttpConfiguration {
/// Port to listen on for HTTP API requests
pub port: u16,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct BlockManagerConfiguration {
/// By default every node is block producer.
///
/// But in trusted settings it might be useful configure only one or selected nodes to be block producer.
/// The rest of nodes still participate in the message gossiping and reliable broadcast.
pub producer: bool,
/// Interval in seconds between block creation
/// Blocks are "proposed" at this interval.
pub creation_interval_sec: u64,
/// Ephemera creates blocks at fixed interval and doesn't have any consensus algorithm to make progress
/// if the most recent block fails to go through reliable broadcast.
/// This flag tells what to do when at next interval previous block is not yet delivered.
///
/// If true, Ephemera will repeat messages from the previous block. Otherwise it will take all messages
/// from mempool as normally.
pub repeat_last_block_messages: bool,
}
impl BlockManagerConfiguration {
pub fn new(producer: bool, creation_interval_sec: u64, repeat_last_block: bool) -> Self {
BlockManagerConfiguration {
producer,
creation_interval_sec,
repeat_last_block_messages: repeat_last_block,
}
}
}
#[derive(Debug, Error)]
pub enum Error {
/// This is returned if configuration file exists and user tries to create new one.
#[error("Configuration file exists: '{0}'")]
Exists(String),
/// This is returned if configuration file does not exist and user tries to load it.
#[error("Configuration file does not exists: '{0}'")]
NotFound(String),
#[error("Configuration file does not exist")]
/// This is returned if IoError happens during configuration file read/write.
Io(#[from] std::io::Error),
/// This is returned if configuration file is invalid.
#[error("Configuration file is invalid: '{0}'")]
InvalidFormat(String),
/// This is returned if configuration file path is invalid.
#[error("Configuration file path is invalid: '{0}'")]
InvalidPath(String),
/// Technical error happens during parsing.
#[error("{}", .0)]
Other(String),
}
impl From<ConfigError> for Error {
fn from(err: ConfigError) -> Self {
match err {
ConfigError::NotFound(err) => Error::NotFound(err),
ConfigError::PathParse(err) => {
Error::InvalidPath(format!("Invalid path to configuration file: {err:?}",))
}
ConfigError::FileParse { uri, cause } => {
Error::InvalidFormat(format!("Invalid configuration file: {uri:?}: {cause:?}",))
}
_ => Error::Other(err.to_string()),
}
}
}
const EPHEMERA_DIR_NAME: &str = "ephemera";
const EPHEMERA_CONFIG_FILE: &str = "ephemera.toml";
type Result<T> = std::result::Result<T, Error>;
impl Configuration {
/// Tries to read Ephemera node configuration file (`ephemera.toml`) from the given path.
///
/// # Arguments
/// * `path` - Path to the configuration file.
///
/// # Errors
/// Returns an error if the configuration file does not exist or is invalid.
///
/// # Example
/// ```no_run
/// use ephemera::configuration::Configuration;
///
/// let config = Configuration::try_load("/ephemera/ephemera.toml");
/// ```
pub fn try_load<P: Into<PathBuf>>(path: P) -> Result<Configuration> {
let buf = path.into();
log::debug!("Loading configuration from {:?}", buf);
let config = config::Config::builder()
.add_source(config::File::from(buf))
.build()
.map_err(Error::from)?;
config.try_deserialize().map_err(Error::from)
}
/// Tries to read Ephemera node configuration from default
/// default Ephemera configuration directory(`~.nym/ephemera`). Full path resolves
/// as `~/.nym/ephemera/<node_name>/<file>`.
///
/// # Arguments
/// * `node_name` - Name of the node.
/// * `file` - Name of the configuration file.
///
/// # Errors
/// Returns an error if the configuration file does not exist or is invalid.
pub fn try_load_node_from_home_dir(file: &str) -> Result<Configuration> {
let file_path = Self::ephemera_node_dir(None)?.join(file);
Configuration::try_load(file_path)
}
/// Tries to read Ephemera node configuration from default Ephemera configuration directory(`~.nym/ephemera`).
/// Full path resolves as `~/.nym/ephemera/<node_name>/ephemera.toml`.
///
/// # Arguments
/// * `node_name` - Name of the node.
///
/// # Errors
/// Returns an error if the configuration file does not exist or is invalid.
pub fn try_load_from_home_dir() -> Result<Configuration> {
let file_path = Configuration::ephemera_config_file_home(None)?;
let config = config::Config::builder()
.add_source(config::File::from(file_path))
.build()
.map_err(Error::from)?;
config.try_deserialize().map_err(Error::from)
}
/// Tries to write(create) Ephemera node configuration file (`ephemera.toml`) relative to default
/// Ephemera configuration directory(`~.nym/ephemera`). Full path resolves as `~/.nym/ephemera/<node_name>/ephemera.toml`.
///
/// # Arguments
/// * `id` - Id of the node.
///
/// # Errors
/// Returns an error if the configuration file already exists.
///
/// # Panics
/// Panics if the configuration file cannot be written.
pub fn try_write_home_dir(&self, id: Option<&str>) -> Result<()> {
let conf_path = Configuration::ephemera_node_dir(id)?;
if !conf_path.exists() {
std::fs::create_dir_all(conf_path)?;
}
let file_path = Configuration::ephemera_config_file_home(id)?;
if file_path.exists() {
return Err(Error::Exists(file_path.to_str().unwrap().to_string()));
}
self.write(&file_path)?;
Ok(())
}
/// Tries to write(update) Ephemera node configuration file (`ephemera.toml`) relative to default
/// Ephemera configuration directory(`~.nym/ephemera`). Full path resolves as `~/.nym/ephemera/<node_name>/ephemera.toml`.
/// If the file does not exist, update will be refused.
///
/// # Arguments
/// * `node_name` - Name of the node.
///
/// # Errors
/// Returns an error if the configuration file does not exist.
///
/// # Panics
/// Panics if the configuration file cannot be written.
pub fn try_update_home_dir(&self) -> Result<()> {
let file_path = Configuration::ephemera_config_file_home(None)?;
if !file_path.exists() {
error!(
"Configuration file does not exist {}",
file_path.to_str().unwrap()
);
return Err(Error::NotFound(file_path.to_str().unwrap().to_string()));
}
self.write(&file_path)?;
Ok(())
}
/// Returns node configuration file path relative to default Ephemera configuration directory(`~.nym/ephemera`).
/// Full path resolves as `~/.nym/ephemera/<id>/ephemera.toml`.
///
/// # Arguments
/// * `id` - Id of the node.
///
/// # Errors
/// Returns an error if the configuration file path cannot be resolved.
pub fn ephemera_config_file_home(id: Option<&str>) -> Result<PathBuf> {
Ok(Self::ephemera_node_dir(id)?.join(EPHEMERA_CONFIG_FILE))
}
/// Returns default Ephemera configuration directory(`~.nym/ephemera`).
///
/// # Errors
/// Returns an error if the configuration directory cannot be resolved.
pub fn ephemera_root_dir(id: Option<&str>) -> Result<PathBuf> {
let id = id.unwrap_or_default();
dirs::home_dir()
.map(|home| {
home.join(NYM_DIR)
.join(DEFAULT_NYM_APIS_DIR)
.join(id)
.join(EPHEMERA_DIR_NAME)
})
.ok_or(Error::Other("Could not find home directory".to_string()))
}
pub(crate) fn ephemera_node_dir(id: Option<&str>) -> Result<PathBuf> {
Self::ephemera_root_dir(id)
}
fn write(&self, file_path: &PathBuf) -> Result<()> {
//TODO: use toml or config crate, not both
let config = toml::to_string(&self).map_err(|e| {
Error::InvalidFormat(format!("Failed to serialize configuration: {e}",))
})?;
let config = format!(
"#This file is generated by cli and automatically overwritten every time when cli is run\n{config}",
);
if file_path.exists() {
info!("Updating configuration file: '{}'", file_path.display());
} else {
info!("Writing configuration to file: '{}'", file_path.display());
}
let mut file = std::fs::File::create(file_path)?;
file.write_all(config.as_bytes())?;
Ok(())
}
}