use std::fmt::{Display, Formatter};
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use anyhow::{bail, Result};
#[cfg(feature = "clap")]
use clap::Parser;
use serde::{Deserialize, Serialize};
use crate::runner::spawn_remote_workers;
use crate::scheduler::HostId;
use crate::CoordUInt;
pub const HOST_ID_ENV_VAR: &str = "NOIR_HOST_ID";
pub const CONFIG_ENV_VAR: &str = "NOIR_CONFIG";
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum RuntimeConfig {
Local(LocalConfig),
Remote(RemoteConfig),
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct LocalConfig {
pub num_cores: CoordUInt,
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct RemoteConfig {
#[serde(skip)]
pub host_id: Option<HostId>,
pub hosts: Vec<HostConfig>,
pub tracing_dir: Option<PathBuf>,
#[serde(default)]
pub cleanup_executable: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct HostConfig {
pub address: String,
pub base_port: u16,
pub num_cores: CoordUInt,
#[serde(default)]
pub ssh: SSHConfig,
pub perf_path: Option<PathBuf>,
}
#[derive(Clone, Serialize, Deserialize, Derivative, Eq, PartialEq)]
#[derivative(Default)]
#[allow(clippy::upper_case_acronyms)]
pub struct SSHConfig {
#[derivative(Default(value = "22"))]
#[serde(default = "ssh_default_port")]
pub ssh_port: u16,
pub username: Option<String>,
pub password: Option<String>,
pub key_file: Option<PathBuf>,
pub key_passphrase: Option<String>,
}
impl std::fmt::Debug for SSHConfig {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RemoteHostSSHConfig")
.field("ssh_port", &self.ssh_port)
.field("username", &self.username)
.field("password", &self.password.as_ref().map(|_| "REDACTED"))
.field("key_file", &self.key_file)
.field(
"key_passphrase",
&self.key_passphrase.as_ref().map(|_| "REDACTED"),
)
.finish()
}
}
#[cfg(feature = "clap")]
#[derive(Debug, Parser)]
#[clap(
name = "noir",
about = "Network of Operators In Rust",
trailing_var_arg = true
)]
pub struct CommandLineOptions {
#[clap(short, long)]
remote: Option<PathBuf>,
#[clap(short, long)]
local: Option<CoordUInt>,
args: Vec<String>,
}
impl RuntimeConfig {
#[cfg(feature = "clap")]
pub fn from_args() -> (RuntimeConfig, Vec<String>) {
let opt: CommandLineOptions = CommandLineOptions::parse();
opt.validate();
let mut args = opt.args;
args.insert(0, std::env::args().next().unwrap());
if let Some(num_cores) = opt.local {
(Self::local(num_cores), args)
} else if let Some(remote) = opt.remote {
(Self::remote(remote).unwrap(), args)
} else {
unreachable!("Invalid configuration")
}
}
pub fn local(num_cores: CoordUInt) -> RuntimeConfig {
RuntimeConfig::Local(LocalConfig { num_cores })
}
pub fn remote<P: AsRef<Path>>(config: P) -> Result<RuntimeConfig> {
let mut config = if let Some(config) = RuntimeConfig::config_from_env() {
config
} else {
log::info!("reading config from: {}", config.as_ref().display());
let content = std::fs::read_to_string(config)?;
serde_yaml::from_str(&content)?
};
for (host_id, host) in config.hosts.iter().enumerate() {
if host.ssh.password.is_some() && host.ssh.key_file.is_some() {
bail!("Malformed configuration: cannot specify both password and key file on host {}: {}", host_id, host.address);
}
}
config.host_id = RuntimeConfig::host_id_from_env(config.hosts.len().try_into().unwrap());
log::debug!("runtime configuration: {config:#?}");
Ok(RuntimeConfig::Remote(config))
}
fn host_id_from_env(num_hosts: CoordUInt) -> Option<HostId> {
let host_id = match std::env::var(HOST_ID_ENV_VAR) {
Ok(host_id) => host_id,
Err(_) => return None,
};
let host_id = match HostId::from_str(&host_id) {
Ok(host_id) => host_id,
Err(e) => panic!("Invalid value for environment {HOST_ID_ENV_VAR}: {e:?}"),
};
if host_id >= num_hosts {
panic!(
"Invalid value for environment {}: value too large, max possible is {}",
HOST_ID_ENV_VAR,
num_hosts - 1
);
}
Some(host_id)
}
fn config_from_env() -> Option<RemoteConfig> {
match std::env::var(CONFIG_ENV_VAR) {
Ok(config) => {
info!("reading remote config from env {}", CONFIG_ENV_VAR);
let config: RemoteConfig =
serde_yaml::from_str(&config).expect("Invalid configuration from environment");
Some(config)
}
Err(_) => None,
}
}
pub fn spawn_remote_workers(&self) {
match &self {
RuntimeConfig::Local(_) => {}
#[cfg(feature = "ssh")]
RuntimeConfig::Remote(remote) => {
spawn_remote_workers(remote.clone());
}
#[cfg(not(feature = "ssh"))]
RuntimeConfig::Remote(_) => {
panic!("spawn_remote_workers() requires the `ssh` feature for remote configs.");
}
}
}
pub fn host_id(&self) -> Option<HostId> {
match self {
RuntimeConfig::Local(_) => Some(0),
RuntimeConfig::Remote(remote) => remote.host_id,
}
}
}
impl Display for HostConfig {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}:{}-]", self.address, self.base_port)
}
}
#[cfg(feature = "clap")]
impl CommandLineOptions {
fn validate(&self) {
if !(self.remote.is_some() ^ self.local.is_some()) {
panic!("Use one of --remote or --local");
}
if let Some(threads) = self.local {
if threads == 0 {
panic!("The number of cores should be positive");
}
}
}
}
fn ssh_default_port() -> u16 {
22
}