use std::env;
use std::ffi::OsStr;
use std::fs::{File, create_dir_all};
use std::io::{BufReader, Write};
use std::path::Path;
use clap::Parser;
use ipnet::Ipv4Net;
use log::LevelFilter;
use log4rs::Config;
use log4rs::append::console::ConsoleAppender;
use log4rs::append::file::FileAppender;
use log4rs::config::{Appender, Root};
use serde::{Deserialize, Serialize};
use crate::OptResult;
use crate::error::OptError;
use crate::general::random_vr_name;
const DEFAULT_JSON_CONFIG: &[u8; 201] = b"
{
\"name\": \"VR_1\",
\"vrid\": 51,
\"interface_name\": \"wlo1\",
\"ip_addresses\": [
\"192.168.100.100/24\"
],
\"priority\": 101,
\"advert_interval\": 1,
\"preempt_mode\": true
}
";
fn default_priority() -> u8 {
100
}
fn default_advert_int() -> u8 {
1
}
fn default_preempt_mode() -> bool {
true
}
fn default_action() -> Action {
Action::Run
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct FileConfig {
pub vrid: u8,
pub ip_addresses: Vec<String>,
pub interface_name: String,
#[serde(default = "random_vr_name")]
pub name: String,
#[serde(default = "default_priority")]
pub priority: u8,
#[serde(default = "default_advert_int")]
pub advert_interval: u8,
#[serde(default = "default_preempt_mode")]
pub preempt_mode: bool,
#[serde(default = "default_action")]
pub action: Action,
}
#[derive(Debug, Clone)]
pub struct CliConfig {
pub name: Option<String>,
pub vrid: u8,
pub ip_addresses: Vec<String>,
pub interface_name: String,
pub priority: u8,
pub advert_interval: u8,
pub preempt_mode: bool,
pub action: Action,
}
#[derive(Debug, Clone)]
pub struct BaseConfig {
pub name: Option<String>,
pub vrid: u8,
pub ip_addresses: Vec<Ipv4Net>,
pub interface_name: String,
pub priority: u8,
pub advert_interval: u8,
pub preempt_mode: bool,
pub action: Action,
}
#[derive(Debug, Clone)]
pub enum VrrpConfig {
File(FileConfig),
Cli(CliConfig),
}
impl VrrpConfig {
pub fn name(&self) -> String {
let name = match self {
VrrpConfig::File(config) => Some(config.name.clone()),
VrrpConfig::Cli(config) => config.name.clone(),
};
match name {
Some(n) => n,
None => random_vr_name(),
}
}
pub fn vrid(&self) -> u8 {
match self {
VrrpConfig::File(config) => config.vrid,
VrrpConfig::Cli(config) => config.vrid,
}
}
pub fn ip_addresses(&self) -> Vec<String> {
match self {
VrrpConfig::File(config) => config.ip_addresses.clone(),
VrrpConfig::Cli(config) => config.ip_addresses.clone(),
}
}
pub fn interface_name(&self) -> String {
match self {
VrrpConfig::File(config) => config.interface_name.clone(),
VrrpConfig::Cli(config) => config.interface_name.clone(),
}
}
pub fn priority(&self) -> u8 {
match self {
VrrpConfig::File(config) => config.priority,
VrrpConfig::Cli(config) => config.priority,
}
}
pub fn advert_interval(&self) -> u8 {
match self {
VrrpConfig::File(config) => config.advert_interval,
VrrpConfig::Cli(config) => config.advert_interval,
}
}
pub fn preempt_mode(&self) -> bool {
match self {
VrrpConfig::File(config) => config.preempt_mode,
VrrpConfig::Cli(config) => config.preempt_mode,
}
}
pub fn action(&self) -> Action {
match self {
VrrpConfig::File(config) => config.action.clone(),
VrrpConfig::Cli(config) => config.action.clone(),
}
}
}
#[derive(Parser, Debug)]
#[command(name = "Version")]
#[command(about = "Runs the VRRP protocol", long_about = None)]
pub struct CliArgs2 {
#[command(
subcommand,
help = "`run` for starting the VRRP instances and `teardown` for stopping them"
)]
cfg: Cfg,
}
#[derive(Parser, Debug)]
enum Cfg {
Run {
#[command(subcommand)]
mode: Mode,
},
Teardown {
#[command(subcommand)]
mode: Mode,
},
}
#[derive(Parser, Debug)]
enum Mode {
FileMode {
#[arg(long, help = "path to the we will get our configs from")]
filename: Option<String>,
#[arg(
long,
default_value = None,
help = "Path log file you want to use"
)]
log_file_path: Option<String>,
},
CliMode {
#[arg(
long,
help = "The name of the Virtual Router Instance. e.g `VR_1`"
)]
name: Option<String>,
#[arg(
long,
help = "Virtual Router ID of the Virtual router instance. "
)]
vrid: u8,
#[arg(long, num_args=1.., help="The IP Address(es) of that will the Virtual router will be assigned.")]
ip_address: Vec<String>,
#[arg(
long,
help = "name of the network interface where the Virtual Router instance will be attached."
)]
interface_name: String,
#[arg(
long,
default_value = "100",
help = "The priority of this instance of the Virtual Router, maximum of 255. The higher priority is chosen to be MASTER."
)]
priority: u8,
#[arg(
long,
default_value = "1",
help = "Interval(in seconds) between which the priodic advert updates are sent (when MASTER). Also used to calculate MasterDown interval when in BACKUP state."
)]
advert_interval: u8,
#[arg(
long,
action,
help = "(highly advised to be true). When true, the higher priority will always preempt the lower priority."
)]
preempt_mode: bool,
#[arg(
long,
default_value = None,
help = "Path log file you want to use"
)]
log_file_path: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Action {
Run,
Teardown,
}
pub fn parse_cli_opts(args: CliArgs2) -> OptResult<Vec<VrrpConfig>> {
match args.cfg {
Cfg::Run { mode } => load_mode(mode, Action::Run),
Cfg::Teardown { mode } => load_mode(mode, Action::Teardown),
}
}
fn load_mode(mode: Mode, action: Action) -> OptResult<Vec<VrrpConfig>> {
match mode {
Mode::FileMode {
filename,
log_file_path,
} => {
configure_logging(log_file_path);
let fpath = match filename {
None => {
match env::var("SNAP_COMMON") {
Ok(path) => path + "/vrrp-config.json",
Err(_) => "/etc/failover/vrrp-config.json".to_string(),
}
}
Some(f) => f,
};
log::info!("using config file {:#?}", fpath);
if !Path::new(&fpath).exists() {
let mut file = match File::create(&fpath) {
Ok(f) => f,
Err(_) => {
let dir_path =
match std::path::Path::new(&fpath).parent() {
Some(dir) => dir,
None => {
return Err(OptError(String::from(
"unable to find config path",
)));
}
};
let _ = create_dir_all(dir_path);
File::create(&fpath).unwrap()
}
};
let _ = file.write_all(DEFAULT_JSON_CONFIG);
}
let mut configs: Vec<VrrpConfig> = vec![];
match read_json_config(&fpath) {
Ok(vec_config) => {
for mut config_item in vec_config {
config_item.action = action.clone();
configs.push(VrrpConfig::File(config_item));
}
}
Err(_) => {
return Result::Err(OptError(format!(
"Problem parsing file {}",
&fpath
)));
}
}
Ok(configs)
}
Mode::CliMode {
mut name,
vrid,
ip_address,
interface_name,
priority,
advert_interval,
preempt_mode,
log_file_path,
} => {
configure_logging(log_file_path);
if name.is_none() {
name = Some(random_vr_name());
}
let config = CliConfig {
name,
vrid,
ip_addresses: ip_address,
interface_name,
priority,
advert_interval,
preempt_mode,
action,
};
Ok(vec![VrrpConfig::Cli(config)])
}
}
}
fn configure_logging(log_file_path: Option<String>) {
let log_console_stderr = ConsoleAppender::builder().build();
let mut log_builder = Config::builder().appender(
Appender::builder().build("stderr", Box::new(log_console_stderr)),
);
let mut root_builder = Root::builder();
if let Some(file_path) = log_file_path {
let log_file = FileAppender::builder().build(file_path).unwrap();
log_builder = log_builder
.appender(Appender::builder().build("logfile", Box::new(log_file)));
root_builder = root_builder.appender("logfile");
}
root_builder = root_builder.appender("stderr");
let log_config = log_builder
.build(root_builder.build(LevelFilter::Debug))
.unwrap();
let _handler = log4rs::init_config(log_config);
}
fn read_json_config<P: AsRef<Path>>(path: P) -> OptResult<Vec<FileConfig>> {
let path_str = path.as_ref().as_os_str();
let file = match File::open(path_str) {
Ok(f) => f,
Err(_) => {
return Err(OptError(format!(
"unable to open file `{:?}`",
path.as_ref().as_os_str()
)));
}
};
let reader = BufReader::new(file);
let mut result: Vec<FileConfig> = Vec::new();
let list_file_configs: Vec<FileConfig> =
match serde_json::from_reader(reader) {
Ok(config) => config,
Err(_) => single_file_config(path_str)?,
};
for file_config in list_file_configs {
if let Some(_con) = result
.iter()
.find(|r: &&FileConfig| r.name == file_config.name)
{
continue;
};
if let Some(_con) = result
.iter()
.find(|r: &&FileConfig| r.vrid == file_config.vrid)
{
continue;
};
result.push(file_config);
}
Ok(result)
}
fn single_file_config(path: &OsStr) -> OptResult<Vec<FileConfig>> {
let file = File::open(path).unwrap();
let reader = BufReader::new(file);
let _: FileConfig = match serde_json::from_reader(reader) {
Ok(config) => return Ok(vec![config]),
Err(_) => {
return Err(OptError(format!(
"Wrong config formatting in file {:?}",
path
)));
}
};
}