diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..efcda5a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "clds" +version = "0.1.0" +authors = ["JesusPerez "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +anyhow = "1.0.40" +chrono = "0.4.19" +envmnt = "0.9.0" +flate2 ="1.0.20" +openssh = "0.8.0" +regex = "1.4.3" +serde = { version = "1.0", features = ["derive"] } +serde_derive = "1.0" +serde_json = "1.0" +serde_yaml = "0.8.17" +tempfile = "3.2.0" +tar = "0.4.33" +tera = "1.8.0" +tokio = { version = "1.5.0", features = ["full"] } +rfm = "0.8.0" +reqenv = { version = "0.1.0", path = "../handlers/reqenv" } diff --git a/src/clouds.rs b/src/clouds.rs new file mode 100644 index 0000000..460bf65 --- /dev/null +++ b/src/clouds.rs @@ -0,0 +1,3 @@ +pub mod defs; +pub mod on_clouds; +pub mod upcloud; diff --git a/src/clouds/defs.rs b/src/clouds/defs.rs new file mode 100644 index 0000000..c077979 --- /dev/null +++ b/src/clouds/defs.rs @@ -0,0 +1,295 @@ +use std::collections::HashMap; +use serde::{Serialize,Deserialize}; + +use crate::pkgs::{PkgInfo}; +use crate::defs::{TskSrvcName,IsCritical,AppName,Cntrllr}; +// use crate::cmds::ssh; +// use tempfile::tempfile; + +#[derive(Clone,Debug,Serialize,Deserialize,Default)] +pub struct ConfigTskSrvc { + pub name: Option, + pub files: HashMap, +} + +#[allow(clippy::missing_docs_in_private_items)] +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct Provider { + pub name: String, + pub runner: String, + pub args: String, +} +impl Provider { + pub fn new(name: String, runner: String, args: String) -> Self { + Self { + name, + runner, + args, + } + } +} + +#[allow(clippy::missing_docs_in_private_items)] +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct CloudEnv { + pub path: String, + pub home: String, + pub monitor_run: String, + pub config_path: String, + pub config_json_path: String, + pub config_root: String, + pub root_tsksrvcs: String, + pub provision: String, + pub cloud: String, + pub group: String, + pub target: String, + pub source: String, + pub source_path: String, + pub tpls_path: String, + pub tsksrvcs_path: String, + pub wk_path: String, + pub pkgs_list: String, + pub versions: String, + pub listhosts: String, + pub force: u8, + key: String, +} + +impl CloudEnv { + pub fn new(force: u8, key: String) -> Self { + Self { + path: String::from(""), + home: String::from(""), + monitor_run: String::from(""), + config_root: String::from(""), + config_path: String::from(""), + config_json_path: String::from(""), + root_tsksrvcs: String::from(""), + tsksrvcs_path: String::from(""), + provision: String::from(""), + cloud: String::from(""), + group: String::from(""), + target: String::from(""), + source: String::from(""), + source_path: String::from(""), + tpls_path: String::from(""), + wk_path: String::from(""), + pkgs_list: String::from(""), + versions: String::from(""), + listhosts: String::from(""), + force, + key, + } + } + pub fn get_key(&self) -> String { + self.key.to_owned() + } +} + +#[allow(clippy::missing_docs_in_private_items)] +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct Cluster { + pub role: String, +} + +#[allow(clippy::missing_docs_in_private_items)] +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct SSH { + pub host: String, + pub keyPath: String, + pub user: String, + pub password: String, + pub port: u16, +} +#[allow(clippy::missing_docs_in_private_items)] +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct Networks { + pub access: String, + pub family: String, + pub network: Option, + pub ipaddress: Option, +} + +#[allow(clippy::missing_docs_in_private_items)] +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct StorageDevice { + pub action: String, + pub title: String, + pub storage: String, + pub size: u16, + pub finalSsize: u16, + pub partSizes: Option, + pub makefs: Option, + pub tier: String, +} +#[allow(clippy::missing_docs_in_private_items)] +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct LoginUser { + pub sshKeys: Vec, +} + +#[allow(clippy::missing_docs_in_private_items)] +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct Server { + pub hostname: String, + pub title: String, +// pub tplPkgs: Option, + pub cluster: Cluster, + pub ssh: SSH, + pub tags: Vec, + pub networks: Vec, + pub useFloatIP: bool, + pub loginUser: LoginUser, + pub plan: String, + pub metadata: String, + pub userData: Option, + pub timeZone: String, + pub zone: String, + pub storageDevices: Vec, + pub tsksrvcs: Vec, +// pub packages: Option>>, +} + +#[allow(non_snake_case)] +pub struct Dhall { + pub templatedLicense: String, +} + +// #[allow(clippy::missing_docs_in_private_items)] +// #[allow(non_snake_case)] +// #[derive(Clone, Debug, Serialize, Deserialize, Default)] +// pub struct Server { +// pub name: String, +// pub tpl: Option, +// pub provider: Option, +// pub arch: Option, +// pub spec: Option, +// } + +#[allow(clippy::missing_docs_in_private_items)] +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct CfgRs { + pub floatIP: Option, + pub hostPrefix: String, + pub mainName: String, + pub provider: Option, + pub domainName: Option, +} + +#[allow(clippy::missing_docs_in_private_items)] +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct ConfigResources { + pub floatIP: Option, + pub hostPrefix: String, + pub mainName: String, + pub provider: Option, + pub domainName: Option, + pub cntrllrs: Vec, + pub servers: Vec, +} + +#[allow(clippy::missing_docs_in_private_items)] +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct DeployConfig { + pub floatIP: Option, + pub hostPrefix: String, + pub mainName: String, + pub domainName: Option, + pub Servers: Vec, +} + +#[allow(clippy::missing_docs_in_private_items)] +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct TskSrvc { + pub name: TskSrvcName, + pub path: String, + pub req: String, + pub target: String, + pub liveness: String, + pub critical: IsCritical, +} + +#[allow(clippy::missing_docs_in_private_items)] +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct App { + pub name: AppName, + pub path: String, + pub req: String, + pub target: String, + pub liveness: String, + pub critical: IsCritical, +} + +// #[allow(clippy::missing_docs_in_private_items)] +// #[derive(Clone, Debug, Serialize, Deserialize, Default)] +// #[allow(non_snake_case)] +// pub struct TskSrvcs { +// pub grpName: String, +// pub tsksrvcsList: Vec, +// pub state: String, +// pub config: String, +// pub log: Vec, +// } + +#[allow(clippy::missing_docs_in_private_items)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[allow(non_snake_case)] +pub struct MainResourcesConfig { + pub mainName: String, + pub provider: String, + pub domainName: String, +} + +#[allow(clippy::missing_docs_in_private_items)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[allow(non_snake_case)] +pub struct Cloud { + pub env: CloudEnv, + pub providers: HashMap, + pub state: String, + pub config_resources: ConfigResources, + pub full_config: DeployConfig, + pub tpl: Vec, + pub pkgs: HashMap, + pub log: Vec, +} +impl Cloud { + pub async fn load_providers() -> HashMap { + let mut providers: HashMap = HashMap::new(); + providers.insert( "upcloud".to_string(), + Provider::new("upcloud".to_string(),"upclapi".to_string(),"-kdr tc -c".to_string()) + ); + providers.insert( "manual".to_string(), + Provider::new("manual".to_string(),"".to_string(),"".to_string()) + ); + providers + } +} +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct TsksrvcInfo { + pub name: String, + pub info: serde_yaml::Value, +} +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct TsksrvcsHostInfo { + pub hostname: String, + pub tsksrvcs: Vec, +} +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct HostInfo { + pub hostname: String, + pub info: String, +} \ No newline at end of file diff --git a/src/clouds/on_clouds.rs b/src/clouds/on_clouds.rs new file mode 100644 index 0000000..e9051f3 --- /dev/null +++ b/src/clouds/on_clouds.rs @@ -0,0 +1,638 @@ +use anyhow::{anyhow,Result,Context, Error}; +use std::{fs}; //,io}; +use std::fs::OpenOptions; +use std::io::{Write}; +use std::path::Path; +use rfm::mkdir; +use std::str; +use std::process::{Command}; +// use std::ffi::OsString; + +use reqenv::ReqEnv; +use crate::utils::{liveness_check}; +use crate::clouds::defs::{ + CloudEnv, + Cloud, + Provider, + TskSrvc, + MainResourcesConfig, +}; +use crate::defs::{KloudHome,KloudCheckHome,SSHAccess,Cntrllr,CloudGroup,CloudCheckGroup,CloudItem,CloudCheckItem}; +use crate::clouds::defs::{TsksrvcInfo}; + +use crate::clouds::upcloud::{get_upcloud_info,run_on_upcloud}; + +/// On_cloud +/// load __`item`__ form `envmnt` with `dflt` +/// Check if path from `source` exits for `item` or fallback to check from `root` path. +/// __`item`__ path is requiered to exist +pub async fn get_env_path(item: &str, dflt: &str, source: &str, root: &str, is_tpl: bool) -> Result { + let mut base = dflt.to_owned(); + if item.len() > 0 { + base=envmnt::get_or(item,dflt); + } + if Path::new(&base).has_root() { + if Path::new(&base).exists() { + return Ok(base); + } else { + return Err(anyhow!("Path {} not found", &base)); + } + } + #[allow(unused_assignments)] + let mut item_path= String::from(""); + if source.len() > 0 { + item_path = format!("{}/{}",&source,&base); + } else { + item_path = format!("{}",&base); + } + if ! Path::new(&item_path).exists() { + item_path=format!("{}/{}",root,&base); + if is_tpl && ! Path::new(&item_path).exists() { + item_path=format!("{}/{}",&source,&base); + if ! Path::new(&item_path).exists() { + item_path=format!("{}/{}",&root,&base); + } + } + if ! Path::new(&item_path).exists() { + return Err(anyhow!("Path '{}' not found in {} - {}", &base,&source,&root)); + } + } + // println!("Path: {} src {} root {} item {}", &base, &source, &root, item_path); + Ok(base) + // Ok(item_path) +} +/// env_cloud +/// Scanning environment from __CLOUD_PATH__ overloaded with __CLOUD_HOME__ `source` CliOpts argument (-s) in case +/// Load path from `envmnt` and __`get_env_path`___ +/// __`source`__ path is mandatory +pub async fn env_cloud(source: &str, cloud_env: &mut CloudEnv) -> Result<()> { + cloud_env.path = envmnt::get_or("ROOT_KLDS", ""); + if cloud_env.path.is_empty() { + return Err(anyhow!("Clouds Root Path {} not found", &cloud_env.source_path)); + } + cloud_env.home=get_env_path("KLDS_HOME","home", "", &cloud_env.path,false).await?; + cloud_env.monitor_run=envmnt::get_or("KLD_MONITOR_RUN","kloud_mon"); + cloud_env.source=source.to_owned(); + cloud_env.config_root = envmnt::get_or("KLD_CONFIG_ROOT", "config"); + if source == "*" { + cloud_env.config_path = envmnt::get_or("KLD_CONFIG", "config.yaml"); + cloud_env.config_json_path = envmnt::get_or("KLD_CONFIG_JSON", "config.json"); + return Ok(()) + } + let arr_source: Vec<&str> = source.split_terminator("/").collect(); + if let Some(name) = arr_source.get(0) { + cloud_env.cloud= format!("{}",&name); + } + if let Some(group) = arr_source.get(1) { + cloud_env.group = format!("{}",&group); + } + if let Some(target) = arr_source.get(2) { + cloud_env.target = format!("{}",&target); + } + cloud_env.source_path = format!("{}/{}",&cloud_env.home,&source); + if ! Path::new(&cloud_env.source_path).exists() { + return Err(anyhow!("Clouds Source Path {} not found", &cloud_env.source_path)); + } + cloud_env.config_path=get_env_path("KLDS_CONFIG","config.yaml", + format!("{}/{}",&cloud_env.source_path,&cloud_env.config_root).as_str(), + &cloud_env.path,false).await?; + cloud_env.config_json_path=get_env_path("KLDS_CONFIG_JSON","config.json", + format!("{}/{}",&cloud_env.source_path,&cloud_env.config_root).as_str(), + &cloud_env.path,false).await?; + if cloud_env.group.is_empty() { + cloud_env.provision=format!("{}/{}/{}/{}",&cloud_env.home,&cloud_env.cloud,envmnt::get_or("CLOUD_PROVISION","provision"),&cloud_env.target); + } else { + cloud_env.provision=format!("{}/{}/{}/{}/{}",&cloud_env.home,&cloud_env.cloud,&cloud_env.group,envmnt::get_or("CLOUD_PROVISION","provision"),&cloud_env.target); + } + if ! Path::new(&cloud_env.provision).exists() { + let dir_path_buf = Path::new(&cloud_env.provision).to_path_buf(); + let dirs: Vec<&std::path::PathBuf> = vec![&dir_path_buf]; + mkdir(&dirs) + .with_context(|| format!("\nFailed to create dir path {}", &cloud_env.provision))?; + //fs::create_dir(&cloud_env.provision)?; + println!("{} created", &cloud_env.provision); + } + cloud_env.wk_path = envmnt::get_or("KLDS_WKDIR", "/tmp"); + // cloud_env.root_tsksrvcs=get_env_path("CLOUDS_ROOT_TSKSRVCS","tsksrvcs", &cloud_env.source_path,&cloud_env.path,false).await?; + // cloud_env.tsksrvcs_path=get_env_path("KLDS_TSKSRVCS","tsksrvcs", &cloud_env.source_path,&cloud_env.path,false).await?; + // cloud_env.tpls_path=get_env_path("KLDS_TPLS","tpls", &cloud_env.source_path,&cloud_env.path,false).await?; + cloud_env.tsksrvcs_path=get_env_path("KLDS_TSKSRVCS","tsksrvcs", &cloud_env.source_path,&cloud_env.path,false).await?; + // cloud_env.pkgs_list=get_env_path("KLDS_PKGS_LIST","pkgs_list.yaml", &cloud_env.source_path,&cloud_env.path,false).await?; + cloud_env.versions=get_env_path("KLDS_VERSIONS","versions.yaml", &cloud_env.source_path,&cloud_env.path,false).await?; + Ok(()) +} +pub async fn clear_specs(source: &str) -> Result<()> { + let env_path = envmnt::get_or("ROOT_KLDS", "clouds"); + let env_home = get_env_path("KLDS_HOME","home", "", &env_path,false).await?; + let env_source=format!("{}/{}",&env_home,&source); + if ! Path::new(&env_source).exists() { + return Ok(()); + } + let env_provision=format!("{}/{}",&env_source,envmnt::get_or("KLDS_PROVISION","provision")); + let env_specs=format!("{}/specs",env_provision); + if Path::new(&env_specs).exists() { + println!("Delete {}/specs",env_provision); + fs::remove_dir_all(&env_specs)?; + } + Ok(()) +} +pub async fn load_config_data(cloud: &Cloud, source: &str) -> Result { + // dbg!(&cloud); + let cfg_path = format!("{}/{}/{}/{}",&cloud.env.home,&source,&cloud.env.config_root,&cloud.env.config_path); + let cfg_data = fs::read_to_string(&cfg_path).with_context(|| format!("Failed to read 'cfg_path' from {}", &cfg_path))?; + Ok(cfg_data) +} +pub async fn load_config_json_data(cloud: &Cloud, source: &str) -> Result { + let cfg_path = format!("{}/{}/{}/{}",&cloud.env.home,&source,&cloud.env.config_root,&cloud.env.config_json_path); + let cfg_data = fs::read_to_string(&cfg_path).with_context(|| format!("Failed to read json 'cfg_path' from {}", &cfg_path))?; + Ok(cfg_data.replace("\n","")) +} +pub async fn load_cloud_env(cloud: &mut Cloud, source: &str) -> Result<()> { + env_cloud(source, &mut cloud.env).await?; + Ok(()) +} +pub async fn load_cloud_config(cloud: &mut Cloud, source: &str) -> Result<(KloudHome,Provider,String), Error> { + // dbg!(&cloud.env); + let cfg_data = load_config_data(&cloud,source).await?; + let cfg: KloudHome = serde_yaml::from_str(&cfg_data)?; + let cfg_provider = format!("{}",cfg.provider[0]); + let provider = cloud.providers.get(&cfg_provider).with_context(|| format!("Provider '{}'' not defined", &cfg_provider))?; + Ok((cfg, provider.to_owned(), cfg_data)) +} +pub async fn load_cloud_check_config(cloud: &mut Cloud, source: &str) -> Result<(KloudCheckHome,Provider,String), Error> { + // dbg!(&cloud.env); + let cfg_data = load_config_data(&cloud,source).await?; + let cfg: KloudCheckHome = serde_yaml::from_str(&cfg_data)?; + let cfg_provider = format!("{}",cfg.provider[0]); + let provider = cloud.providers.get(&cfg_provider).with_context(|| format!("Provider '{}'' not defined", &cfg_provider))?; + Ok((cfg, provider.to_owned(), cfg_data)) +} +pub async fn load_cloud_name_config(cloud: &mut Cloud, source: &str) -> Result<(MainResourcesConfig,Provider,String), Error> { + // dbg!(&cloud.env); + let cfg_data = load_config_data(&cloud,source).await?; + let cfg: MainResourcesConfig = serde_yaml::from_str(&cfg_data)?; + let provider = cloud.providers.get(&cfg.provider).with_context(|| format!("Provider '{}'' not defined", &cfg.provider))?; + Ok((cfg, provider.to_owned(), cfg_data)) +} +pub async fn get_cloud_monitor_info(cloud: &mut Cloud, source: &str) -> Result { + let cloud_home_path = format!("{}/{}",&cloud.env.home,&source); + let monitor_path = format!("{}/{}",&cloud_home_path,&cloud.env.monitor_run); + if Path::new(&monitor_path).exists() { + let output = Command::new("bash") + .arg(format!("{}",&monitor_path)) + .arg("-o") + .arg("json") + .arg(format!("{}",&source)) + .output()?; + if !&output.status.success() { + return Err(anyhow!("Run {} for {} failed: {}",&cloud.env.monitor_run,&source,&output.status)); + } + return Ok(str::from_utf8(&output.stdout).unwrap_or_else(|_| "").to_owned()); + } + Ok("".to_owned()) +} +pub async fn get_cloud_home_list(cloud: &Cloud) -> Result> { + let kloud_files: Vec = fs::read_dir(&cloud.env.home)? + .filter_map(|res| + match res.map(|e| e.file_name()) { + Ok(entry) => { + // for e.path() + // let file_path = entry.as_path().display().to_string(); + let file_path = format!("{}",entry.to_owned().into_string().unwrap_or_else(|_|String::from(""))); + let cfg_path = format!("{}/{}/{}/{}",&cloud.env.home,&file_path,&cloud.env.config_root,&cloud.env.config_path); + let first_char = file_path.chars().next().unwrap_or_default().to_string(); + if first_char.as_str() != "_" && first_char.as_str() != "." && Path::new(&cfg_path).exists() { + Some(file_path) + } else { + None + } + }, + Err(e) => { + eprintln!("Error filter_map {}",e); + None + } + } + ) + .collect(); + Ok(kloud_files.to_owned()) +} + +pub async fn run_ssh_on_srvr(hostname: &str,tsksrvc_name: &str, tsksrvc_cmd: &str, ssh_access: &SSHAccess) -> Result { + println!("Checking connection to {}@{} on {} for {} ",ssh_access.user,ssh_access.host,ssh_access.port,&tsksrvc_name); + println!("ssh {} /var/lib/klouds/bin/{}_info.sh yaml",&hostname,&tsksrvc_name); + let output = Command::new("ssh") + .arg("-q") + .arg("-o") + .arg("StrictHostKeyChecking=accept-new") + .arg("-p") + .arg(format!("{}",ssh_access.port)) + .arg(format!("{}@{}",ssh_access.user,ssh_access.host)) + .arg("sudo") + .arg(format!("/var/lib/klouds/bin/{}_info.sh",&tsksrvc_name)) + .arg("yaml") + .arg(format!("{}",&tsksrvc_cmd)) +// .arg(format!("ssh {} /var/lib/klouds/bin/{}_info.sh yaml",&hostname,&tsksrvc_name)) + .output()?; + //dbg!(&output); + if !&output.status.success() { + return Err(anyhow!("Connection to '{}' for tsksrvc '{}' failed: {}",&hostname,&tsksrvc_name,&output.status)); + } + let res = str::from_utf8(&output.stdout).unwrap_or_else(|_| ""); + let info: serde_yaml::Value = serde_yaml::from_str(&res) + .unwrap_or_else(|e| { + eprintln!("serde_yaml: {}",e); + serde_yaml::Value::default() + }); + Ok(info.to_owned()) +} +pub async fn parse_srvr_tsksrvcs(hostname: &str, sshaccess: &SSHAccess, tsksrvcs: &Vec,req_tsksrvcs: &str) -> Vec { + let mut tsksrvcs_info: Vec = Vec::new(); + for tsk in req_tsksrvcs.split(",") { + match format!("{}",&tsk).as_str() { + "os" => { + let os_name = String::from("os"); + tsksrvcs_info.push( TsksrvcInfo { + name: format!("{}",&os_name), + info: run_ssh_on_srvr(&hostname, &os_name, "", &sshaccess).await + .unwrap_or_else(|e| { + eprintln!("run_ssh_on_srvr os: {}",e); + serde_yaml::Value::default() + }), + }); + }, + "floatip" => { + let floatip_name = String::from("floatip"); + tsksrvcs_info.push( TsksrvcInfo { + name: format!("{}",&floatip_name), + info: run_ssh_on_srvr(&hostname, &floatip_name, "", &sshaccess).await + .unwrap_or_else(|e| { + eprintln!("run_ssh_on_srvr floatip: {}",e); + serde_yaml::Value::default() + }), + }); + }, + "kubernetes_pods" => { + let k8_name = String::from("kubernetes"); + tsksrvcs_info.push(TsksrvcInfo { + name: format!("{}_pods",&k8_name), + info: run_ssh_on_srvr(&hostname, &k8_name, "pods", &sshaccess).await + .unwrap_or_else(|e| { + eprintln!("run_ssh_on_srvr kubernetes_pods: {}",e); + serde_yaml::Value::default() + }), + }); + }, + _ => { continue; } + }; + } + for tsksrvc in tsksrvcs.iter() { + match format!("{}",&tsksrvc.name).as_str() { + "pause" => continue, + "scale" => continue, + "systemfix" => continue, + "os" => continue, + _ => { + let name = format!("{}",&tsksrvc.name); + if req_tsksrvcs == "all" || req_tsksrvcs.contains(&name) { + // TODO ssh &srv.hostname to get &name in "yaml" + //println!("{} {} {}",&hostname,sshaccess.user,&tksrvc.name); + tsksrvcs_info.push(TsksrvcInfo { + name: format!("{}",&tsksrvc.name), + info: run_ssh_on_srvr(&hostname, &name, "", &sshaccess).await + .unwrap_or_else(|e| { + eprintln!("run_ssh_on_srvr for {}: {}",&tsksrvc.name,e); + serde_yaml::Value::default() + }), + }); + } + } + }; + } + tsksrvcs_info.to_owned() +} +pub async fn liveness_srvr_tsksrvcs(source: &str, cntrllrs: &Vec, tsksrvcs: &Vec, req_tsksrvc: &str) -> Vec { + let mut tsksrvcs_info: Vec = Vec::new(); + for tsksrvc in tsksrvcs.iter() { + match format!("{}",&tsksrvc.name).as_str() { + "pause" => continue, + "scale" => continue, + "systemfix" => continue, + _ => { + let name = format!("{}",&tsksrvc.name); + if tsksrvc.liveness.is_empty() || (req_tsksrvc != "" && !req_tsksrvc.contains(&name)) { + continue; + } + let serverstring = format!("{}",&tsksrvc.liveness); + let res_info = match liveness_check(&source,&cntrllrs,&serverstring,&name).await { + Ok(_) => "ok", + Err(e) => { + eprint!("{}",e); + "err" + }, + }; + // println!("{} info: {}",&tsksrvc.name,&res_info); + tsksrvcs_info.push(TsksrvcInfo { + name: format!("{}",&tsksrvc.name), + info: serde_yaml::from_str(res_info).unwrap_or_else(|e| { + eprintln!("Serde liveness Error: {} {} -> {}",&source,&serverstring,e); + serde_yaml::Value::default() + }), + }); + }, + } + } + tsksrvcs_info.to_owned() +} +pub async fn get_provider_info(provider: &str, hostname: &str, cmd: &str , cfg_path: &str) -> String { + match provider { + "upcloud" => { + get_upcloud_info(hostname,cmd,cfg_path).await + }, + _ => String::from("") + } +} + +pub async fn run_on_provider(reqname: &str, req_tsksrvc: &str, req_srvrs: &str, provider: Provider, source: &str, cfg_data: String, env_cloud: &Cloud) -> String { + match provider.name.as_str() { + "upcloud" => { + // TODO clean SSH keys or encrypt content + // dbg!(&cloud_config); + run_on_upcloud(reqname,req_tsksrvc,req_srvrs, source, cfg_data, env_cloud).await + }, + _ => { + let result = format!("Errors on {} provider {} not found",&source,&provider.name); + println!("{}",&result); + result + } + } +} +pub async fn on_cloud_name_req(reqname: &str,env_cloud: &Cloud,_reqenv: &ReqEnv,req_tsksrvc: &str, req_srvrs: &str, source: &str) -> String { + let mut cloud = env_cloud.to_owned(); + load_cloud_env(&mut cloud, &source).await + .unwrap_or_else(|e| { + eprintln!("load_cloud_env: {}",e); + }); + let (cfg,provider,cfg_data) = load_cloud_name_config(&mut cloud, &source).await + .unwrap_or_else(|e| { + eprintln!("load_cloud_name_config: {}",e); + (MainResourcesConfig::default(),Provider::default(),String::from("")) + }); + if cfg.mainName.is_empty() || cfg_data.is_empty() { + let result = format!("Errors loading {}",&source); + println!("{}",&result); + return result; + } + run_on_provider(&reqname,&req_tsksrvc,&req_srvrs,provider,&source,cfg_data,&cloud).await +} +pub async fn create_cloud_config(reqname: &str,req_tsksrvcs: &str,reqenv: &ReqEnv, entries: Vec,mut cloud: Cloud) -> String { + let config = reqenv.config(); + let check_path = format!("{}/clouds.json",&config.check_path); + let mut check_entries: Vec = Vec::new(); + let mut no_check_entries = true; + if reqname != "check_job" { + if Path::new(&check_path).exists() { + // Load & Parse reuse liveness and monitor + let check_data = fs::read_to_string(&check_path).unwrap_or_else(|e|{ + println!("Failed to read 'check_path' from {}: {}", &check_path,e); + String::from("") + }); + if !check_data.is_empty() { + check_entries = serde_json::from_str(&check_data).unwrap_or_else(|e| { + println!("Error loading check_entries ({}): {}",&check_path,e); + Vec::new() + }); + no_check_entries=false; + println!("Using check_entries from {}",&check_path); + // dbg!("{:#?}",&check_entries); + } + } + } + let mut entries_cfgs: Vec = Vec::new(); + for (idx, entry) in entries.iter().enumerate() { + let (mut cfg,_provider,cfg_data) = load_cloud_config(&mut cloud, &entry).await + .unwrap_or_else(|e| { + eprintln!("Load_cloud_config: {}",e); + (KloudHome::default(),Provider::default(),String::from("")) + }); + if cfg.name.is_empty() || cfg_data.is_empty() { + let result = format!("Errors loading {}",&entry); + println!("{}",&result); + continue; + } + if req_tsksrvcs.contains("monitor") { + if no_check_entries { + cfg.monitor_info = Some(get_cloud_monitor_info(&mut cloud, &entry).await + .unwrap_or_else(|e| { + eprintln!("Error {} monitor_info {} -> {}",&entry,&cloud.env.monitor_run,e); + String::from("") + })); + } else { + cfg.monitor_info = check_entries[idx].monitor_info.to_owned(); + } + } + if reqname.contains("provision") || req_tsksrvcs.contains("resources") || req_tsksrvcs.contains("resources") || req_tsksrvcs.contains("liveness") { + let mut groups: Vec = Vec::new(); + // cfg.groups = cfg.groups.map(|grp| grp.with_resources(grp.path.to_owned())).collect(); + for (grp_idx,grp) in cfg.groups.iter().enumerate() { + let mut items: Vec = Vec::new(); + for (itm_idx,itm) in grp.items.iter().enumerate() { + let resources: Option; + let liveness: Option; + let provision: Option; + if req_tsksrvcs.contains("liveness") { + if no_check_entries { + liveness = Some(on_cloud_name_req("liveness",&cloud,&reqenv,"","",&itm.path).await); + } else { + liveness = check_entries[idx].groups[grp_idx].items[itm_idx].liveness.to_owned(); + } + } else { + liveness = itm.liveness.to_owned(); + } + if reqname.contains("provision") || req_tsksrvcs.contains("provision") { + provision = Some(on_cloud_name_req("provision",&cloud,&reqenv,"","",&itm.path).await); + } else { + provision = itm.provision.to_owned(); + } + if req_tsksrvcs.contains("resources") { + resources = Some(load_config_json_data(&cloud,&itm.path).await + .unwrap_or_else(|e| { + eprintln!("Error loading resources -> {}",e); + String::from("") + })); + } else { + resources = itm.resources.to_owned(); + } + items.push(CloudItem { + name: itm.name.to_owned(), + info: itm.info.to_owned(), + path: itm.path.to_owned(), + resources, + liveness, + provision, + graph: itm.graph.to_owned(), + critical: itm.critical.to_owned(), + }); + } + groups.push(CloudGroup { + name: grp.name.to_owned(), + info: grp.info.to_owned(), + path: grp.path.to_owned(), + // TODO check this for group + resources: grp.resources.to_owned(), + liveness: grp.liveness.to_owned(), + provision: grp.provision.to_owned(), + items, + graph: grp.graph.to_owned(), + prices: grp.prices.to_owned(), + }); + } + cfg.groups=groups; + } + entries_cfgs.push(cfg.to_owned()); + } + serde_json::to_string(&entries_cfgs).unwrap_or_else(|_| String::from("")).replace("\n","") +} + +pub async fn create_cloud_check(req_tsksrvcs: &str,reqenv: &ReqEnv,entries: Vec,mut cloud: Cloud) -> String { + let mut cfg_entries: Vec = Vec::new(); + for entry in entries.iter() { + let (mut cfg,_provider,cfg_data) = load_cloud_check_config(&mut cloud, &entry).await + .unwrap_or_else(|e| { + eprintln!("load_cloud_check_config: {}",e); + (KloudCheckHome::default(),Provider::default(),String::from("")) + }); + if cfg.name.is_empty() || cfg_data.is_empty() { + let result = format!("Errors loading {}",&entry); + println!("{}",&result); + continue; + } + if req_tsksrvcs.contains("monitor") { + cfg.monitor_info = Some(get_cloud_monitor_info(&mut cloud, &entry).await + .unwrap_or_else(|e| { + eprintln!("Error {} monitor_info {} -> {}",&entry,&cloud.env.monitor_run,e); + String::from("") + })); + } + if req_tsksrvcs.contains("liveness") { + let mut groups: Vec = Vec::new(); + // cfg.groups = cfg.groups.map(|grp| grp.with_resources(grp.path.to_owned())).collect(); + for grp in cfg.groups.iter() { + let mut items: Vec = Vec::new(); + for itm in grp.items.iter() { + let liveness: Option; + if req_tsksrvcs.contains("liveness") { + liveness = Some(on_cloud_name_req("liveness",&cloud,&reqenv,"","",&itm.path).await); + } else { + liveness = itm.liveness.to_owned(); + } + items.push(CloudCheckItem { + name: itm.name.to_owned(), + info: itm.info.to_owned(), + path: itm.path.to_owned(), + liveness, + critical: itm.critical.to_owned(), + }); + } + groups.push(CloudCheckGroup { + name: grp.name.to_owned(), + info: grp.info.to_owned(), + path: grp.path.to_owned(), + // TODO check this for group + liveness: grp.liveness.to_owned(), + items, + }); + } + cfg.groups=groups; + } + cfg_entries.push(cfg.to_owned()); + } + serde_json::to_string(&cfg_entries).unwrap_or_else(|_| String::from("")).replace("\n","") +} +pub async fn on_cloud_req(reqname: &str,env_cloud: &Cloud,reqenv: &ReqEnv,req_tsksrvcs: &str,_req_srvrs: &str, source: &str) -> String { + //println!("{}",&reqname); + let config = reqenv.config(); + // let lock_path = format!("{}/{}_{}{}",&config.cache_lock_path,&reqname,&req_tsksrvcs.replace(",","_"),&config.cache_lock_ext); + // if Path::new(&lock_path).exists() || reqname.ends_with("_job") { + if ! reqname.ends_with("_job") { + let output_path = format!("{}/{}_{}.json",&config.cache_path,&reqname,&req_tsksrvcs.replace(",","_")); + if Path::new(&output_path).exists() { + println!("Using cache: {} at {}",&output_path,envmnt::get_or(format!("LAST_CACHE_{}",&output_path), "")); + let output_data = fs::read_to_string(&output_path).with_context(|| format!("Failed to read json cache 'outut_path' from {}", &output_path)) + .unwrap_or_else(|e| { + eprintln!("read file {}: {}",&output_path,e); + String::from("") + }); + return output_data; + } + } + let mut cloud = env_cloud.to_owned(); + load_cloud_env(&mut cloud, &source).await + .unwrap_or_else(|e| { + eprintln!("load_cloud_env {}",e); + }); + let entries: Vec; + if source == "*" { + entries = get_cloud_home_list(&cloud).await + .unwrap_or_else(|e| { + eprintln!("get_cloud_home_list: {}",e); + Vec::new() + }); + } else { + entries = vec!(source.to_string()); + } + if reqname == "check_job" { + create_cloud_check(req_tsksrvcs,reqenv,entries,cloud).await + } else { + create_cloud_config(reqname,req_tsksrvcs,reqenv,entries,cloud).await + } +} +pub async fn get_cloud_cache_req(reqenv: &ReqEnv,cloud: &Cloud, reqname: &str, reqname_job: &str, tsksrvcs: &str) -> Result<()> { + println!("cloud cache {} ... {:?} ",reqname,chrono::Utc::now()); + let config = reqenv.config(); + let lock_path = format!("{}/{}_{}.{}",&config.cache_lock_path,&reqname,&tsksrvcs.replace(",","_"),&config.cache_lock_ext); + let output_path = format!("{}/{}_{}.json",&config.cache_path,&reqname,&tsksrvcs.replace(",","_")); + if Path::new(&lock_path).exists() { + if envmnt::get_or(format!("LAST_CACHE_{}",&output_path),"") != "" { + println!("Lock found {} ",&lock_path); + // return Err(anyhow!("Lock found {} ",&lock_path)); + return Ok(()) + } else { + println!("Not LAST_CACHE environment found for lock: {} ",&lock_path); + } + } + // println!("Lock NOT found {} ",&lock_path); + let now = chrono::Utc::now().timestamp(); + envmnt::set(format!("LAST_CACHE_{}",&output_path), format!("{}",&now)); + let result = on_cloud_req(&reqname_job,&cloud,&reqenv,tsksrvcs,"","*").await; + if Path::new(&output_path).exists() { + fs::remove_file(&output_path)?; + } + let mut file = OpenOptions::new().write(true).create(true).open(&output_path)?; + file.write_all(result.as_bytes())?; + let out = format!("{}: [cloud config] -> {}\n",&now,&output_path); + println!("{}",&out); + Ok(()) +} +pub async fn make_cloud_cache(reqenv: &ReqEnv,cloud: &Cloud) -> Result<()> { + println!("Making cloud cache {:?} ... ",chrono::Utc::now()); + get_cloud_cache_req(reqenv,cloud, "config", "config_job", "monitor,resources,liveness,provision").await.unwrap_or_else(|e| println!("Error cache config: {}",e)); + //get_cloud_cache_req(reqenv,cloud, "provision", "provision_job", "").await.unwrap_or_else(|e| println!("Error cache provision: {}",e)); + Ok(()) +} +pub async fn run_clouds_check(reqenv: &ReqEnv,cloud: &Cloud) -> Result<()> { + println!("cloud check ... {:?} ",chrono::Utc::now()); + let config = reqenv.config(); + let output_path = format!("{}/clouds.json",&config.check_path); + let now = chrono::Utc::now().timestamp(); + envmnt::set(format!("LAST_CHECK{}",&output_path), format!("{}",&now)); + let result = on_cloud_req("check_job",&cloud,&reqenv,"monitor,liveness","","*").await; + if Path::new(&output_path).exists() { + fs::remove_file(&output_path)?; + } + let mut file = OpenOptions::new().write(true).create(true).open(&output_path)?; + file.write_all(result.as_bytes())?; + let out = format!("{}: [cloud check] -> {}\n",&now,&output_path); + println!("{}",&out); + Ok(()) +} +// let debug=envmnt::get_isize("DEBUG",0); \ No newline at end of file diff --git a/src/clouds/upcloud.rs b/src/clouds/upcloud.rs new file mode 100644 index 0000000..3da9094 --- /dev/null +++ b/src/clouds/upcloud.rs @@ -0,0 +1,154 @@ +use anyhow::{anyhow,Result}; +use std::str; +use std::process::{Command}; +use std::path::Path; + +use crate::clouds::defs::{Cloud}; +use crate::providers::defs::upcloud::{ResourcesConfig,ConfigResources}; +use crate::providers::upcloud::{parse_resources_cfg,make_config_resources}; +use crate::clouds::defs::{TsksrvcsHostInfo,HostInfo}; +use crate::clouds::on_clouds::{parse_srvr_tsksrvcs,liveness_srvr_tsksrvcs}; + +pub async fn load_upcloud_config(cfg_data: String) -> Result { + let mut res_cfg: ResourcesConfig = serde_yaml::from_str(&cfg_data)?; + parse_resources_cfg(&mut res_cfg).await?; + let config_res: ConfigResources = make_config_resources(&mut res_cfg).await?; + Ok(config_res) +} + +pub async fn upclapi_run(cmd: &str, id: &str, cfg_path: &str) -> Result { + let upclapi_path = envmnt::get_or("UPCLAPI", "upclapi"); + if ! Path::new(&upclapi_path).exists() { + return Err(anyhow!("upclapi not found in {}",&upclapi_path)); + } + let output: std::process::Output; + if id.is_empty() { + output = Command::new(&upclapi_path) + .arg("-c") + .arg(format!("{}",&cmd)) + .arg("-f") + .arg(format!("{}",&cfg_path)) + .output()?; + } else { + output = Command::new(&upclapi_path) + .arg("-c") + .arg(format!("{}",&cmd)) + .arg("-id") + .arg(format!("{}",&id)) + .arg("-f") + .arg(format!("{}",&cfg_path)) + .output()?; + } + if !&output.status.success() { + return Err(anyhow!("upclapi run -c {} -id {} -s {} failed: {}",&cmd,&id,&cfg_path,&output.status)); + } + let res = str::from_utf8(&output.stdout).unwrap_or_else(|_| ""); + Ok(res.replace("\n","").to_owned()) +} +pub async fn get_upcloud_info(hostname: &str, cmd: &str,cfg_path: &str) -> String { + match cmd { + "server" => { + upclapi_run("infoserver",&hostname,"").await + .unwrap_or_else(|e| { + eprintln!("get_upcloud_info infoserver {}: {}",&hostname,e); + String::from("") + }) + }, + "floatip" => { + upclapi_run("infofloatip","", &cfg_path).await + .unwrap_or_else(|e| { + eprintln!("get_upcloud_info floatio {}: {}",&hostname,e); + String::from("") + }) + }, + _ => { String::from("")} + } +} +pub async fn run_on_upcloud(reqname: &str,req_tsksrvc: &str, req_srvrs: &str, source: &str, cfg_data: String, env_cloud: &Cloud) -> String { + let cloud_config = load_upcloud_config(cfg_data).await + .unwrap_or_else(|e| { + eprintln!("load_upcloud_config: {}",e); + ConfigResources::default() + }); + match reqname { + "config" => { + serde_json::to_string(&cloud_config) + .unwrap_or_else(|e| { eprintln!("{}",e); String::from("")}) + }, + "provision" => { + let mut hosts_info: Vec = Vec::new(); + let cfg_path= format!("{}/{}/config.yaml",&env_cloud.env.home,&source); + match req_tsksrvc { + "floatip" => { + hosts_info.push(HostInfo { + hostname: "floatip".to_owned(), + info: get_upcloud_info(&cloud_config.servers[0].hostname,&req_tsksrvc,&cfg_path).await, + }); + }, + _ => { + for srvr in cloud_config.servers.iter() { + if req_srvrs == "" || req_srvrs.contains(&srvr.hostname) { + hosts_info.push(HostInfo { + hostname: srvr.hostname.to_owned(), + info: get_upcloud_info(&srvr.hostname,"server",&cfg_path).await, + }); + } + }; + } + }; + serde_json::to_string(&hosts_info) + .unwrap_or_else(|e| { eprintln!("{}",e); String::from("")}) + }, + "status" => { + match req_tsksrvc { + _ => { + let mut hosts_tsksrvcs_info: Vec = Vec::new(); + for srvr in cloud_config.servers.iter() { + if req_srvrs == "" || req_srvrs.contains(&srvr.hostname) { + // let srvr=&cloud_config.servers[0]; + // serde_json::to_string(&cloud_config).unwrap_or_else(|_| String::from("")) + // let str_host = format!("- hostname: {}\n tsksrvcs:\n",&srvr.hostname); + // res.push_str(&str_host); + // TODO check if is alive + // res.push_str( + hosts_tsksrvcs_info.push(TsksrvcsHostInfo { + hostname: srvr.hostname.to_owned(), + tsksrvcs: parse_srvr_tsksrvcs(&srvr.hostname, &srvr.sshAccess, &srvr.tsksrvcs,&req_tsksrvc).await, + }); + // ); + // res.push('\n'); + } + }; + // serde_json::to_string(&cloud_config).unwrap_or_else(|_| String::from("")) + // res.to_owned() + // println!("{}",&res); + // let tsks_yaml: Vec = serde_yaml::from_str(&res) + // .unwrap_or_else(|e| { eprintln!("{}",e); Vec::new() }); + serde_json::to_string(&hosts_tsksrvcs_info) + .unwrap_or_else(|e| { eprintln!("{}",e); String::from("")}) + } + } + } + "liveness" => { + match req_tsksrvc { + _ => { + let mut hosts_tsksrvcs_info: Vec = Vec::new(); + for srvr in cloud_config.servers.iter() { + if req_srvrs == "" || req_srvrs.contains(&srvr.hostname) { + hosts_tsksrvcs_info.push(TsksrvcsHostInfo { + hostname: srvr.hostname.to_owned(), + tsksrvcs: liveness_srvr_tsksrvcs(&source,&cloud_config.cntrllrs,&srvr.tsksrvcs,&req_tsksrvc).await, + }); + } + }; + serde_json::to_string(&hosts_tsksrvcs_info) + .unwrap_or_else(|e| { eprintln!("serde liveness: {}",e); String::from("")}) + } + } + }, + _ => { + serde_json::to_string(&cloud_config) + .unwrap_or_else(|e| { eprintln!("{}",e); String::from("")}) + }, + } +} diff --git a/src/cmds.rs b/src/cmds.rs new file mode 100644 index 0000000..e2f1e22 --- /dev/null +++ b/src/cmds.rs @@ -0,0 +1,82 @@ +// use openssh::{Session, KnownHosts}; +use openssh::*; +//use std::io; +//use std::process::Stdio; +use tokio::io::{AsyncWriteExt,AsyncReadExt}; + +/// ssh +/// ````rust +/// // Prepare address +/// let host= String::from("hostname-or-ip"); +/// let user= String::from("root"); +/// let port: u16 = 22; +/// let addr = format!("ssh://{}@{}:{}",&user,&host,port); +/// +/// // for scp_to_add data content into /tmp/hola +/// let tsksrvc= String::from("scp_to_add"); +/// let trgt=String::from("/tmp/hola"); +/// let mut data = String::from("Dime \n"); +// +/// // for ssh ls /tmp +/// let tsksrvc= String::from("ssh"); +/// let trgt=String::from("ls"); +/// let mut data = String::from("/tmp"); +/// +/// // Call command and "macth" result +/// match cmds::ssh(&tsksrvc, &addr, &trgt, &mut data) { +/// Ok(rs) => println!("ssh res: {:?} -> {:?}", rs, &data), +/// Err(e) => println!("ssh error: {:?}", e), +/// } +/// ``` +// #[tokio::main] +pub async fn ssh(tsksrvc: &str, addr: &str, trgt: &str, data: &mut String ) -> anyhow::Result<()> { + + let session = Session::connect(&addr,KnownHosts::Strict).await?; + + if tsksrvc == "ssh" { + let ls = session.command(trgt).arg(data).output().await?; + match String::from_utf8(ls.stdout) { + Ok(res) => println!("ok : {:?}",&res), + Err(e) => println!("Error {:?}",e), + }; + } else { + let mut sftp = session.sftp(); + match tsksrvc { + "scp_to" => { + let mut w = sftp.write_to(trgt).await?; + let content = data.as_bytes(); + w.write_all(content).await?; + w.close().await?; + }, + "scp_to_add" => { + let mut w = sftp.append_to(trgt).await?; + let content = data.as_bytes(); + w.write_all(content).await?; + w.close().await?; + }, + "scp_from" => { + let mut r = sftp.read_from(trgt).await?; + r.read_to_string(data).await?; + // println!("source: {:?}",&data); + r.close().await?; + }, + _ => println!("Undefined {:?}",&tsksrvc), + }; + } + session.close().await?; + Ok(()) +} +// println!("SSH error no KeyPair found"); +// .map_err(|e| { +// debug!("e = {:?}", e); +// Error::SendError +// })?; + +// Command::new("ctar") +// .arg("-C") +// .arg(&output_root_path) +// .arg("czf") +// .arg(format!("{}/{}.tar.gz",&cloud.env.wk_path,&hostname)) +// .arg("*") +// .spawn() +// .expect("command failed"); \ No newline at end of file diff --git a/src/defs.rs b/src/defs.rs new file mode 100644 index 0000000..020e5cd --- /dev/null +++ b/src/defs.rs @@ -0,0 +1,779 @@ +use serde::{Serialize, Deserialize, Deserializer}; +use std::fmt; + +use crate::clouds::on_clouds::load_config_data; +use crate::clouds::defs::Cloud; + +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct LoginUser { + pub sshKeys: Vec, +} + +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct Cluster { + pub role: String, +} + +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct NetworkInterface { + pub access: String, + pub family: String, + pub network: Option, + pub ipaddress: Option, +} +impl Default for NetworkInterface { + fn default() -> Self { + Self { + access: String::from("public"), + family: String::from("IPv4"), + network: None, + ipaddress: None, + } + } +} + +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SSHAccess { + pub keyPath: String, + pub password: String, + pub port: u16, + pub host: String, + pub utype: String, + pub user: String, +} +impl Default for SSHAccess { + fn default() -> Self { + Self { + keyPath: String::from(""), + password: String::from(""), + port: 22, + host: String::from(""), + utype: String::from("key"), + user: String::from("root"), + } + } +} + +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct StorageDevice { + pub action: String, + pub size: u16, + pub finalSize: u16, + pub storage: String, + pub partSizes: Option, + pub makefs: Option, + pub tier: String, + pub title: String, + pub source: String, +} +impl Default for StorageDevice { + fn default() -> Self { + Self { + action: String::from("clone"), + size: 5, + finalSize: 5, + partSizes: None, + makefs: None, + storage: String::from(""), + tier: String::from("maxiops"), + title: String::from(""), + source: String::from(""), + } + } +} + +#[derive(Eq, PartialEq, Copy, Clone, Debug, Serialize, Deserialize)] +#[allow(non_camel_case_types)] +pub enum TskSrvcName { + os, + systemfix, + sysconfig, + tools, + proxy, + coredns, + etcd, + vpn, + keepalived, + containerd, + crio, + podman, + kubernetes, + k3s, + webhook, + cloudmandala, + zterton, + repo, + postgres, + nfs, + redis, + local, + devel, + scale, + klouds, + pause, +} + +impl Default for TskSrvcName { + fn default() -> Self { + TskSrvcName::os + } +} + +impl fmt::Display for TskSrvcName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TskSrvcName::os => write!(f,"os"), + TskSrvcName::systemfix => write!(f,"systemfix"), + TskSrvcName::sysconfig => write!(f,"sysconfig"), + TskSrvcName::tools => write!(f,"tools"), + TskSrvcName::proxy => write!(f,"proxy"), + TskSrvcName::etcd => write!(f,"etcd"), + TskSrvcName::vpn => write!(f,"vpn"), + TskSrvcName::coredns => write!(f,"coredns"), + TskSrvcName::keepalived => write!(f,"keepalived"), + TskSrvcName::crio => write!(f,"crio"), + TskSrvcName::containerd => write!(f,"containerd"), + TskSrvcName::kubernetes => write!(f,"kubernetes"), + TskSrvcName::k3s => write!(f,"k3s"), + TskSrvcName::webhook => write!(f,"webhook"), + TskSrvcName::podman => write!(f,"podman"), + TskSrvcName::repo => write!(f,"repo"), + TskSrvcName::postgres => write!(f,"postgres"), + TskSrvcName::redis => write!(f,"redis"), + TskSrvcName::local => write!(f,"local"), + TskSrvcName::scale => write!(f,"scale"), + TskSrvcName::devel => write!(f,"devel"), + TskSrvcName::klouds => write!(f,"klouds"), + TskSrvcName::cloudmandala => write!(f,"cloudmandala"), + TskSrvcName::zterton => write!(f,"zterton"), + TskSrvcName::nfs => write!(f,"nfs"), + TskSrvcName::pause => write!(f,"pause"), + } + } +} + +impl TskSrvcName { + pub fn set_systsksrvc(pack: String) -> TskSrvcName { + match pack.as_str() { + "os" => TskSrvcName::os, + "systemfix" => TskSrvcName::systemfix, + "sysconfig" => TskSrvcName::sysconfig, + "tools" => TskSrvcName::tools, + "proxy" => TskSrvcName::proxy, + "coredns" => TskSrvcName::coredns, + "etcd" => TskSrvcName::etcd, + "vpn" => TskSrvcName::vpn, + "keepalived" => TskSrvcName::keepalived, + "containerd" => TskSrvcName::containerd, + "crio" => TskSrvcName::crio, + "podman" => TskSrvcName::podman, + "kubernetes" => TskSrvcName::kubernetes, + "k3s" => TskSrvcName::k3s, + "webhook" => TskSrvcName::webhook, + "cloudmandala" => TskSrvcName::cloudmandala, + "zterton" => TskSrvcName::zterton, + "nfs" => TskSrvcName::nfs, + "repo" => TskSrvcName::repo, + "postgres" => TskSrvcName::postgres, + "redis" => TskSrvcName::redis, + "local" => TskSrvcName::local, + "devel" => TskSrvcName::devel, + "scale" => TskSrvcName::scale, + "klouds" => TskSrvcName::klouds, + "pause" => TskSrvcName::pause, + &_ => TskSrvcName::default(), + } + } +} + +#[derive(Eq, PartialEq, Copy, Clone, Debug, Serialize, Deserialize)] +#[allow(non_camel_case_types)] +pub enum AppName { + none, + registry, + repo, + wp_site, + mariadb, + postgres, + redis, + mail, + gitea, + local, + zterton, +} + +impl Default for AppName { + fn default() -> Self { + AppName::none + } +} + +impl fmt::Display for AppName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AppName::none => write!(f,"none"), + AppName::registry => write!(f,"registry"), + AppName::repo => write!(f,"repo"), + AppName::wp_site => write!(f,"wp_site"), + AppName::mariadb => write!(f,"mariadb"), + AppName::postgres => write!(f,"postgres"), + AppName::redis => write!(f,"redis"), + AppName::mail => write!(f,"mail"), + AppName::gitea => write!(f,"gitea"), + AppName::local => write!(f,"local"), + AppName::zterton => write!(f,"zterton"), + } + } +} + +impl AppName { + pub fn set_appname(pack: String) -> AppName { + match pack.as_str() { + "none" => AppName::none, + "registry" => AppName::registry, + "repo" => AppName::repo, + "wp_site" => AppName::wp_site, + "mariadb" => AppName::mariadb, + "postgres" => AppName::postgres, + "redis" => AppName::redis, + "mail" => AppName::mail, + "gitea" => AppName::gitea, + "local" => AppName::local, + "zterton" => AppName::zterton, + &_ => AppName::default(), + } + } +} + +#[derive(Eq, PartialEq, Copy, Clone, Debug, Serialize, Deserialize)] +#[allow(non_camel_case_types)] +pub enum ProviderName { + none, + manual, + upcloud, +} + +impl Default for ProviderName { + fn default() -> Self { + ProviderName::none + } +} + +impl fmt::Display for ProviderName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ProviderName::none => write!(f,"none"), + ProviderName::manual => write!(f,"manual"), + ProviderName::upcloud => write!(f,"upcloud"), + } + } +} + +#[derive(Eq, PartialEq, Copy, Clone, Debug, Serialize, Deserialize)] +#[allow(non_camel_case_types)] +pub enum InfaceMode { + none, + float, + alias, +} +impl InfaceMode { + pub fn set_infacemode(pack: String) -> InfaceMode { + match pack.as_str() { + "none" => InfaceMode::none, + "float" => InfaceMode::float, + "alias" => InfaceMode::alias, + &_ => InfaceMode::default(), + } + } +} + +impl Default for InfaceMode { + fn default() -> Self { + InfaceMode::none + } +} + +impl fmt::Display for InfaceMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InfaceMode::none => write!(f,"none"), + InfaceMode::float => write!(f,"float"), + InfaceMode::alias => write!(f,"alias"), + } + } +} + +#[derive(Eq, PartialEq, Copy, Clone, Debug, Serialize, Deserialize)] +#[allow(non_camel_case_types)] +pub enum InfaceNet { + none, + public, + sdn, + prv, + vpn, +} +impl InfaceNet { + pub fn set_infacenet(pack: String) -> InfaceNet { + match pack.as_str() { + "none" => InfaceNet::none, + "public" => InfaceNet::public, + "pub" => InfaceNet::public, + "sdn" => InfaceNet::sdn, + "prv" => InfaceNet::prv, + "vpn" => InfaceNet::vpn, + &_ => InfaceNet::default(), + } + } +} +impl Default for InfaceNet { + fn default() -> Self { + InfaceNet::none + } +} +impl fmt::Display for InfaceNet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InfaceNet::none => write!(f,"none"), + InfaceNet::public => write!(f,"pub"), + InfaceNet::sdn => write!(f,"sdn"), + InfaceNet::prv => write!(f,"prv"), + InfaceNet::vpn => write!(f,"vpn"), + } + } +} +#[derive(Eq, PartialEq, Copy, Clone, Debug, Serialize, Deserialize)] +#[allow(non_camel_case_types)] +pub enum NetworkMethodName { + none, + firstavailable, +} +impl NetworkMethodName { + pub fn set_providername(pack: String) -> NetworkMethodName { + match pack.as_str() { + "none" => NetworkMethodName::none, + "firstavailable" => NetworkMethodName::firstavailable, + &_ => NetworkMethodName::default(), + } + } +} +impl Default for NetworkMethodName { + fn default() -> Self { + NetworkMethodName::none + } +} +impl fmt::Display for NetworkMethodName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + NetworkMethodName::none => write!(f,"none"), + NetworkMethodName::firstavailable => write!(f,"firstavailable"), + } + } +} +#[derive(Eq, PartialEq, Copy, Clone, Debug, Serialize, Deserialize)] +#[allow(non_camel_case_types)] +pub enum IsCritical { + no, + yes, + cloud, + group, + ifresized, +} +impl IsCritical { + pub fn set_infacenet(pack: String) -> IsCritical { + match pack.as_str() { + "no" => IsCritical::no, + "yes" => IsCritical::yes, + "cloud" => IsCritical::cloud, + "group" => IsCritical::group, + "ifresized" => IsCritical::ifresized, + &_ => IsCritical::default(), + } + } +} +impl Default for IsCritical { + fn default() -> Self { + IsCritical::no + } +} +impl fmt::Display for IsCritical { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + IsCritical::no => write!(f,"no"), + IsCritical::yes => write!(f,"yes"), + IsCritical::cloud => write!(f,"cloud"), + IsCritical::group => write!(f,"group"), + IsCritical::ifresized => write!(f,"ifresized"), + } + } +} +#[derive(Eq, PartialEq, Copy, Clone, Debug, Serialize, Deserialize)] +#[allow(non_camel_case_types)] +pub enum EdgeDirection { + directional, + unidirectional, + bidirectional, +} +impl EdgeDirection { + pub fn set_infacenet(pack: String) -> EdgeDirection { + match pack.as_str() { + "directional" => EdgeDirection::directional, + "unidirectional" => EdgeDirection::unidirectional, + "bidirectional" => EdgeDirection::bidirectional, + &_ => EdgeDirection::default(), + } + } +} +impl Default for EdgeDirection { + fn default() -> Self { + EdgeDirection::directional + } +} +impl fmt::Display for EdgeDirection { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + EdgeDirection::directional => write!(f,"directional"), + EdgeDirection::unidirectional => write!(f,"unidirectional"), + EdgeDirection::bidirectional => write!(f,"bidirectional"), + } + } +} +fn deserialize_infacenet<'de, D>(deserializer: D) -> Result +where D: Deserializer<'de> { + let buf = String::deserialize(deserializer)?; + // let res = String::from_str(&buf).map_err(serde::de::Error::custom) { + // .unwrap_or_else(|| serde::de::Error::custom); + Ok(InfaceNet::set_infacenet(buf)) +} +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct IpDef { + pub ip: String, + pub hostname: String, + pub inface: String, + pub mode: InfaceMode, + pub methods: Vec, + #[serde(deserialize_with = "deserialize_infacenet")] + pub inet: InfaceNet, + pub ports: Vec, + pub aliveport: String, + pub targets: Vec, + pub monitor: String, + pub critical: IsCritical, + pub graph: GraphNode, +} +impl Default for IpDef { + fn default() -> Self { + Self { + ip: String::from(""), + hostname: String::from(""), + inface: String::from(""), + mode: InfaceMode::default(), + methods: vec!(NetworkMethodName::default()), + inet: InfaceNet::default(), + ports: Vec::new(), + aliveport: String::from(""), + targets: Vec::new(), + monitor: String::from(""), + critical: IsCritical::no, + graph: GraphNode::default(), + } + } +} +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Cntrllr { + pub host: String, + pub sshaccess: SSHAccess, + pub cldPath: String, + pub masterPath: String, +} +impl Default for Cntrllr { + fn default() -> Self { + Self { + host: String::from(""), + sshaccess: SSHAccess::default(), + cldPath: String::from(""), + masterPath: String::from(""), + } + } +} +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Price { + pub item: String, + pub amount: i64, + pub month: f64, + pub hour: f64, +} +impl Default for Price { + fn default() -> Self { + Self { + item: String::from(""), + amount: 1, + month: 0.0, + hour: 0.0, + } + } +} +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CloudItem { + pub name: String, + pub path: String, + pub info: String, + pub resources: Option, + pub liveness: Option, + pub provision: Option, + pub critical: IsCritical, + pub graph: GraphNode, +} +impl Default for CloudItem { + fn default() -> Self { + Self { + name: String::from(""), + path: String::from(""), + info: String::from(""), + resources: Some(String::from("")), + liveness: Some(String::from("")), + provision: Some(String::from("")), + critical: IsCritical::no, + graph: GraphNode::default(), + } + } +} +impl CloudItem { + #[allow(clippy::missing_errors_doc)] + pub async fn load_resources(&self,cloud: &Cloud) -> anyhow::Result { + // dbg!("{}/{}",&home_path,&self.path); + Ok(load_config_data(&cloud, &self.path).await + .unwrap_or_else(|e|{ + eprintln!("CloudGroup error loading resources {} -> {}",&self.path,e); + String::from("") + }) + ) + } + #[allow(clippy::missing_errors_doc)] + pub async fn with_resources(&self,cloud: &Cloud) -> Self { + Self { + name: self.name.to_owned(), + path: self.path.to_owned(), + info: self.info.to_owned(), + resources: Some(self.load_resources(cloud).await.unwrap_or_else(|_|String::from(""))), + liveness: self.liveness.to_owned(), + provision: self.provision.to_owned(), + graph: self.graph.to_owned(), + critical: self.critical.to_owned(), + } + } +} +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CloudGroup { + pub name: String, + pub path: String, + pub info: String, + pub resources: Option, + pub liveness: Option, + pub provision: Option, + pub items: Vec, + pub graph: GraphNode, + pub prices: Vec, +} +impl Default for CloudGroup { + fn default() -> Self { + Self { + name: String::from(""), + path: String::from(""), + info: String::from(""), + resources: Some(String::from("")), + liveness: Some(String::from("")), + provision: Some(String::from("")), + items: Vec::new(), + graph: GraphNode::default(), + prices: Vec::new(), + } + } +} +impl CloudGroup { + #[allow(clippy::missing_errors_doc)] + pub async fn load_resources(&self,cloud: &Cloud) -> anyhow::Result { + // dbg!("{}/{}",&home_path,&self.path); + Ok(load_config_data(&cloud, &self.path).await + .unwrap_or_else(|e|{ + eprintln!("CloudGroup error loading resources {} -> {}",&self.path,e); + String::from("") + }) + ) + } + #[allow(clippy::missing_errors_doc)] + pub async fn with_resources(&self,cloud: &Cloud) -> Self { + Self { + name: self.name.to_owned(), + path: self.path.to_owned(), + info: self.info.to_owned(), + resources: Some(self.load_resources(cloud).await.unwrap_or_else(|_|String::from(""))), + liveness: self.liveness.to_owned(), + provision: self.liveness.to_owned(), + items: self.items.to_owned(), + graph: self.graph.to_owned(), + prices: self.prices.to_owned(), + } + } +} +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct KloudHome { + pub name: String, + pub title: String, + pub dflts: String, + pub provider: Vec, + pub netips: Vec, + pub cntrllrs: Vec, + pub groups: Vec, + pub monitor_info: Option, + pub graph: GraphNode, + pub prices: Vec, +} +impl Default for KloudHome { + fn default() -> Self { + Self { + name: String::from(""), + title: String::from(""), + dflts: String::from(""), + provider: vec!(ProviderName::default()), + netips: vec!(IpDef::default()), + cntrllrs: vec!(Cntrllr::default()), + groups: vec!(CloudGroup::default()), + monitor_info: Some(String::from("")), + graph: GraphNode::default(), + prices: vec!(Price::default()), + } + } +} +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CloudCheckItem { + pub name: String, + pub path: String, + pub info: String, + pub liveness: Option, + pub critical: IsCritical, +} +impl Default for CloudCheckItem { + fn default() -> Self { + Self { + name: String::from(""), + path: String::from(""), + info: String::from(""), + liveness: Some(String::from("")), + critical: IsCritical::no, + } + } +} +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CloudCheckGroup { + pub name: String, + pub path: String, + pub info: String, + pub liveness: Option, + pub items: Vec, +} +impl Default for CloudCheckGroup { + fn default() -> Self { + Self { + name: String::from(""), + path: String::from(""), + info: String::from(""), + liveness: Some(String::from("")), + items: Vec::new(), + } + } +} +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct KloudCheckHome { + pub name: String, + pub title: String, + pub dflts: String, + pub provider: Vec, + pub netips: Vec, + pub cntrllrs: Vec, + pub groups: Vec, + pub monitor_info: Option, +} +impl Default for KloudCheckHome { + fn default() -> Self { + Self { + name: String::from(""), + title: String::from(""), + dflts: String::from(""), + provider: vec!(ProviderName::default()), + netips: vec!(IpDef::default()), + cntrllrs: vec!(Cntrllr::default()), + groups: vec!(CloudCheckGroup::default()), + monitor_info: Some(String::from("")), + } + } +} + +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct GraphEdge { + pub name: String, + pub direction: EdgeDirection, + pub origin: String, + pub target: String, + pub text: String, + pub color: String, + pub text_color: String, +} +impl Default for GraphEdge { + fn default() -> Self { + Self { + name: String::from(""), + direction: EdgeDirection::default(), + origin: String::from(""), + target: String::from(""), + text: String::from(""), + color: String::from(""), + text_color: String::from(""), + } + } +} + +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct GraphNode { + pub name: String, + pub parent: String, + pub text: String, + pub x: i32, + pub y: i32, + pub n: i32, + pub color: String, + pub bg_color: String, + pub text_color: String, + pub edges: Vec, +} +impl Default for GraphNode { + fn default() -> Self { + Self { + name: String::from(""), + parent: String::from(""), + text: String::from(""), + x: 0, + y: 0, + n: 1, + color: String::from(""), + bg_color: String::from(""), + text_color: String::from(""), + edges: Vec::new(), // vec!(GraphEdge::default()), + } + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ba3bbe4 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,7 @@ +pub mod providers; +pub mod defs; +pub mod clouds; +pub mod tsksrvcs; +pub mod pkgs; +pub mod cmds; +pub mod utils; diff --git a/src/pkgs.rs b/src/pkgs.rs new file mode 100644 index 0000000..d07d8f6 --- /dev/null +++ b/src/pkgs.rs @@ -0,0 +1,193 @@ +use serde::{Serialize,Deserialize}; +use std::collections::HashMap; +use std::fs; //, io}; +use std::path::Path; +use anyhow::{Result,Context}; + +use crate::clouds::defs::{Cloud,TskSrvc}; + +#[allow(clippy::missing_docs_in_private_items)] +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct Haproxy { + pub mode: String, +} + +#[allow(clippy::missing_docs_in_private_items)] +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct Coredns { + pub port: u16, +} + +#[allow(clippy::missing_docs_in_private_items)] +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct Etcd { + pub nodePort: u16, + pub port: u16, + pub name: String, + pub cn: String, + pub sslMode: String, + pub dataDir: String, +} + +#[allow(clippy::missing_docs_in_private_items)] +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct Keepalived { + pub mainPort: u16, + pub mainIP: String, + pub services: Vec, +} + +// TODO load in a hashMap +// #[allow(clippy::missing_docs_in_private_items)] +// #[allow(non_snake_case)] +// #[derive(Clone, Debug, Serialize, Deserialize, Default)] +// pub struct PkgsConfig { +// pub haproxy: Option, +// pub coredns: Option, +// pub etcd: Option, +// pub keepalived: Option, +// pub name: String, +// pub info: PkgIn, +// } + +#[allow(clippy::missing_docs_in_private_items)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct PkgInfo { + pub vers: String, + pub arch: Option, + pub file: Option, + pub url: Option, + pub url2: Option, + pub prd: Option, + pub pkg: Option, +} + +#[allow(clippy::missing_docs_in_private_items)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct PkgVers { + pub vers: String, + pub prd: Option, + pub install: Option, + pub data: Option, + pub path: Option, +} +/* +#[allow(clippy::missing_docs_in_private_items)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct Kubernetes { + pub k8s: PkgInfo, + pub pkgs: Vec, +} +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct K8sPkgs { + pub name: String, + pub info: PkgVers, +} +// TODO load in a hashMap +/* + pub dashboard: PkgIn, + pub cillium: PkgIn, + pub helm: PkgIn, + pub istio: PkgIn, + pub rook: PkgIn, + pub ceph: PkgIn, + pub tekton: PkgIn, + pub tektonDashboard: PkgIn, + pub tikv: PkgIn, + pub titan: PkgIn, +} +*/ + +#[allow(clippy::missing_docs_in_private_items)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct PkgVers { + pub name: String, + pub info: PkgIn, +} +*/ +#[allow(clippy::missing_docs_in_private_items)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct Packages { + pub pkgs: Vec, +} +// TODO load in a hashMap +// #[allow(clippy::missing_docs_in_private_items)] +// #[derive(Clone, Debug, Serialize, Deserialize, Default)] +// pub struct PkgsVers { +// pub containerd: PkgInfo, +// pub crictl: PkgInfo, +// pub coredns: PkgInfo, +// pub etcd: PkgInfo, +// pub haproxy: PkgInfo, +// pub keepalived: PkgInfo, +// pub restic: PkgInfo, +// pub yq: PkgInfo, +// pub s3cmd: PkgInfo, +// pub kubernetes: Kubernetes, +// } + +pub fn load_pack_info(tsksrvc_vars: &mut HashMap, package: &str, pkg_info: PkgInfo ) { + tsksrvc_vars.insert(format!("{}_vers",package),pkg_info.vers); + tsksrvc_vars.insert(format!("{}_arch",package),pkg_info.arch.unwrap_or(String::from(""))); + tsksrvc_vars.insert(format!("{}_file",package),pkg_info.file.unwrap_or(String::from(""))); + tsksrvc_vars.insert(format!("{}_url",package),pkg_info.url.unwrap_or(String::from(""))); + tsksrvc_vars.insert(format!("{}_url2",package),pkg_info.url2.unwrap_or(String::from(""))); + tsksrvc_vars.insert(format!("{}_prd",package),pkg_info.prd.unwrap_or(String::from(""))); + tsksrvc_vars.insert(format!("{}_pkg",package),pkg_info.pkg.unwrap_or(String::from(""))); +} + +pub async fn get_pkgs_vers(cloud: &mut Cloud) -> Result<()> { + let env_path: String; + if Path::new(&cloud.env.versions).exists() { + env_path=String::from(&cloud.env.versions); + } else { + env_path=format!("{}/{}",&cloud.env.path,&cloud.env.versions); + } + let vers_content= fs::read_to_string(&env_path).with_context(|| format!("Failed to read from {}", &cloud.env.versions))?; + cloud.pkgs = serde_yaml::from_str(&vers_content)?; + Ok(()) +} + +pub fn load_pack_vars(tsksrvc_vars: &mut HashMap, cloud: &Cloud, pkg_path: &str) -> Result<()> { + let pkg_data = fs::read_to_string(&pkg_path) + .with_context(|| format!("Failed to read 'pkg_path' from {}", &pkg_path))?; + let pkg_vars: HashMap = serde_yaml::from_str(&pkg_data)?; + for (key,value) in pkg_vars.iter() { + tsksrvc_vars.insert(key.to_owned(), value.to_owned()); + if key == "req_packs" { + for req_pack in value.split(" ") { + if let Some(pk) = cloud.pkgs.get(req_pack) { + load_pack_info(tsksrvc_vars, &req_pack, pk.to_owned()); + } + } + } + } + Ok(()) +} + +pub fn load_pack_config(tsksrvc_vars: &mut HashMap, cloud: &Cloud, tsk: &TskSrvc, hostname: &str) -> Result<()> { + // print!(" {} ",&tsk.name); + let tsk_name = format!("{}",&tsk.name); + tsksrvc_vars.insert("pack_name".to_string(),tsk_name.to_owned()); + if let Some(pkg) = cloud.pkgs.get(&tsk_name) { + load_pack_info(tsksrvc_vars, &tsk_name, pkg.to_owned()); + } + // let mut pkg_path = format!("{}/home/{}/packs/{}_{}.yaml",&cloud.env.path,&cloud.env.source_pathh,&hostname,&tsk.name); + let mut pkg_path = format!("{}/packs/{}_{}.yaml",&cloud.env.source_path,&hostname,&tsk.name); + if Path::new(&pkg_path).exists() { + load_pack_vars(tsksrvc_vars,cloud,&pkg_path)?; + } + pkg_path = format!("{}/packs/{}.yaml",&cloud.env.source_path,&tsk.name); + if Path::new(&pkg_path).exists() { + load_pack_vars(tsksrvc_vars,cloud,&pkg_path)?; + } + pkg_path = format!("{}/{}/{}.yaml",&cloud.env.tsksrvcs_path,&tsk.name,&tsk.name); + if Path::new(&pkg_path).exists() { + load_pack_vars(tsksrvc_vars,cloud,&pkg_path)?; + } + Ok(()) +} \ No newline at end of file diff --git a/src/providers.rs b/src/providers.rs new file mode 100644 index 0000000..b167ef1 --- /dev/null +++ b/src/providers.rs @@ -0,0 +1,3 @@ +pub mod defs; +pub mod manual; +pub mod upcloud; diff --git a/src/providers/defs.rs b/src/providers/defs.rs new file mode 100644 index 0000000..33dbc67 --- /dev/null +++ b/src/providers/defs.rs @@ -0,0 +1,3 @@ + +pub mod manual; +pub mod upcloud; \ No newline at end of file diff --git a/src/providers/defs/manual.rs b/src/providers/defs/manual.rs new file mode 100644 index 0000000..271676d --- /dev/null +++ b/src/providers/defs/manual.rs @@ -0,0 +1,234 @@ +use serde::{Serialize, Deserialize}; +use crate::defs::{ + Cluster, + LoginUser, + SSHAccess, + Cntrllr, + NetworkInterface, + StorageDevice, +}; +use crate::clouds::defs::{TskSrvc}; + +#[derive(Eq, PartialEq, Copy, Clone, Debug, Serialize, Deserialize)] +#[allow(non_camel_case_types)] +pub enum ServerPlan { + ubuntu_1xCPU_1GB, + manual, +} + +impl Default for ServerPlan { + fn default() -> Self { + ServerPlan::manual + } +} +impl ServerPlan { + pub fn get_plan (&self) -> String { + match self { + ServerPlan::ubuntu_1xCPU_1GB => String::from("1xCPU-1GB"), + ServerPlan::manual => String::from("manual"), + } + } +} + +#[allow(non_camel_case_types)] +#[derive(Eq, PartialEq, Copy, Clone,Debug, Serialize, Deserialize)] +pub enum Zone { + local, +} + +impl Default for Zone { + fn default() -> Self { + Zone::local + } +} +impl Zone { + pub fn get_zone (&self) -> String { + match self { + Zone::local => String::from("local"), + } + } +} + +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ServerInstance { + #[serde(default)] + pub cluster: Cluster, + pub hostname: String, + pub title: String, + #[serde(default)] + pub loginUser: LoginUser, + #[serde(default)] + pub metadata: bool, + #[serde(default)] + pub plan: ServerPlan, + #[serde(default)] + pub zone: Zone, + pub arch: String, + #[serde(default)] + pub sshAccess: SSHAccess, + #[serde(default)] + pub networks: Vec, + #[serde(default)] + pub storageDevices: Vec, + #[serde(default)] + pub timeZone: String, + #[serde(default)] + pub tags: Vec, + #[serde(default)] + pub useFloatIP: bool, + #[serde(default)] + pub userData: Option, + pub tsksrvcs: Vec, +} +impl Default for ServerInstance { + fn default() -> Self { + let net: NetworkInterface = NetworkInterface::default(); + let net_basic: NetworkInterface = NetworkInterface::manual_basic(); + let storage = StorageDevice::manual_storage_device_ubuntu20(25); + Self { + cluster: Cluster::default(), + hostname: String::from(""), + title: String::from(""), + loginUser: LoginUser::default(), + metadata: true, + plan: ServerPlan::manual, + zone: Zone::local, + arch: String::from("linux_amd64"), + sshAccess: SSHAccess::default(), + networks: vec![ net, net_basic ], + storageDevices: vec![ storage ], + timeZone: String::from("UTC"), + tags: Vec::new(), + useFloatIP: false, + userData: Some(String::from("")), + tsksrvcs: Vec::new(), + } + } +} + +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SrvrCfg { + #[serde(default)] + pub role: String, + #[serde(default)] + pub sshUser: String, + pub sshKeys: Vec, + #[serde(default)] + pub sshKeyPath: String, + pub storageDeviceSize: u16, + #[serde(default)] + pub tags: Vec, + #[serde(default)] + pub useFloatIP: bool +} +impl Default for SrvrCfg { + fn default() -> Self { + Self { + role: String::from(""), + sshUser: String::from(""), + sshKeys: Vec::new(), + sshKeyPath: String::from("/root/.ssh/id_rsa"), + storageDeviceSize: 25, + tags: Vec::new(), + useFloatIP: false + } + } +} + +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct ResourcesConfig { + pub floatIP: Option, + #[serde(default)] + pub hostPrefix: String, + #[serde(default)] + pub mainName: String, + pub group: String, + pub group_path: String, + pub provider: Option, + pub domainName: Option, + pub cntrllrs: Vec, + #[serde(default)] + pub srvrcfg: SrvrCfg, + #[serde(default)] + pub servers: Vec, +} + +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct ConfigResources { + pub floatIP: Option, + #[serde(default)] + pub hostPrefix: String, + #[serde(default)] + pub mainName: String, + pub group: String, + pub group_path: String, + pub provider: Option, + pub domainName: Option, + pub cntrllrs: Vec, + #[serde(default)] + pub servers: Vec, +} + +// Implementations & settings + +impl StorageDevice { + pub fn manual_storage_device_ubuntu20 (size: u16) -> Self { + Self { + action: String::from("clone"), + storage: String::from("01000000-0000-4000-8000-000030200200"), + size: size, + finalSize: size, + partSizes: Some(String::from("")), + makefs: Some(String::from("")), + title: String::from("Ubuntu Server 20.04 LTS (Focal Fossa)"), + tier: String::from("maxiopsa"), + source: String::from(""), + } + } +} + +impl NetworkInterface { + pub fn manual_basic () -> Self { + let mut basic = NetworkInterface::default(); + basic.access = String::from("utility"); + basic + } + pub fn manual_sdn () -> Self { + Self { + access: String::from("private"), + family: String::from("IPv4"), + network: Some(String::from("03fa354b-3896-4d04-9f54-d8c7b8da654e")), + ipaddress: Some(String::from("")), + } + } +} + +impl ServerInstance { + pub fn server (plan: ServerPlan, zone: Zone, size: u16, arch: &str) -> Self { + let net: NetworkInterface = NetworkInterface::default(); + let net_basic: NetworkInterface = NetworkInterface::manual_basic(); + let storage = StorageDevice::manual_storage_device_ubuntu20(size); + Self { + cluster: Cluster::default(), + hostname: String::from(""), + title: String::from(""), + loginUser: LoginUser::default(), + metadata: true, + plan, + zone, + arch: String::from(arch), + sshAccess: SSHAccess::default(), + networks: vec![ net, net_basic ], + storageDevices: vec![ storage ], + timeZone: String::from("UTC"), + tags: Vec::new(), + useFloatIP: false, + userData: Some(String::from("")), + tsksrvcs: Vec::new(), + } + } +} diff --git a/src/providers/defs/upcloud.rs b/src/providers/defs/upcloud.rs new file mode 100644 index 0000000..22901de --- /dev/null +++ b/src/providers/defs/upcloud.rs @@ -0,0 +1,299 @@ +use serde::{Serialize, Deserialize, Deserializer}; +use anyhow::{Result}; +// use std::str::FromStr; +use crate::defs::{ + Cluster, + LoginUser, + SSHAccess, + NetworkInterface, + StorageDevice, + Cntrllr, +}; +use crate::clouds::defs::{TskSrvc,App}; + +#[derive(Eq, PartialEq, Copy, Clone, Debug, Serialize, Deserialize)] +#[allow(non_camel_case_types)] +pub enum ProviderServerPlan { + upcloud_1xCPU_1GB, + upcloud_1xCPU_2GB, + upcloud_2xCPU_4GB, + upcloud_4xCPU_8GB, + none, +} + +impl Default for ProviderServerPlan { + fn default() -> Self { + ProviderServerPlan::upcloud_2xCPU_4GB + } +} +impl ProviderServerPlan { + pub fn get_plan (&self) -> String { + match self { + ProviderServerPlan::upcloud_1xCPU_1GB => String::from("1xCPU-1GB"), + ProviderServerPlan::upcloud_1xCPU_2GB => String::from("1xCPU-2GB"), + ProviderServerPlan::upcloud_2xCPU_4GB => String::from("2xCPU-4GB"), + ProviderServerPlan::upcloud_4xCPU_8GB => String::from("4xCPU-8GB"), + ProviderServerPlan::none => String::from(""), + } + } + pub fn set_plan (plan: String) -> ProviderServerPlan { + match plan.as_str() { + "1xCPU-1GB" => ProviderServerPlan::upcloud_1xCPU_1GB, + "upcloud_1xCPU_1GB" => ProviderServerPlan::upcloud_1xCPU_1GB, + "1xCPU-2GB" => ProviderServerPlan::upcloud_1xCPU_2GB, + "upcloud_1xCPU_2GB" => ProviderServerPlan::upcloud_1xCPU_2GB, + "2xCPU-4GB" => ProviderServerPlan::upcloud_2xCPU_4GB, + "upcloud_2xCPU_4GB" => ProviderServerPlan::upcloud_2xCPU_4GB, + "4xCPU-8GB" => ProviderServerPlan::upcloud_4xCPU_8GB, + "upcloud_4xCPU_8GB" => ProviderServerPlan::upcloud_4xCPU_8GB, + "" => ProviderServerPlan::none, + &_ => ProviderServerPlan::default(), + } + } +} + +#[allow(non_camel_case_types)] +#[derive(Eq, PartialEq, Copy, Clone,Debug, Serialize, Deserialize)] +pub enum ProviderZone { + upcloud_nl_ams1, + upcloud_es_mad1, + upcloud_de_fra1, + none, +} + +impl Default for ProviderZone { + fn default() -> Self { + ProviderZone::upcloud_nl_ams1 + } +} +impl ProviderZone { + pub fn get_zone (&self) -> String { + match self { + ProviderZone::upcloud_nl_ams1 => String::from("nl-ams1"), + ProviderZone::upcloud_es_mad1 => String::from("es-mad1"), + ProviderZone::upcloud_de_fra1=> String::from("de-fra1"), + ProviderZone::none => String::from(""), + } + } + pub fn set_zone (zone: String) -> ProviderZone { + match zone.as_str() { + "nl-ams1" => ProviderZone::upcloud_nl_ams1, + "upcloud_nl_ams1" => ProviderZone::upcloud_nl_ams1, + "es-mad1" => ProviderZone::upcloud_es_mad1, + "upcloud_es_mad1" => ProviderZone::upcloud_es_mad1, + "de-fra1" => ProviderZone::upcloud_de_fra1, + "upcloud_de_fra1" => ProviderZone::upcloud_de_fra1, + "" => ProviderZone::none, + &_ => ProviderZone::default(), + } + } +} + +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ServerInstance { + #[serde(default)] + pub cluster: Cluster, + pub hostname: String, + pub title: String, + #[serde(default)] + pub loginUser: LoginUser, + #[serde(default)] + pub metadata: i8, + #[serde(deserialize_with = "deserialize_plan")] + pub plan: ProviderServerPlan, + #[serde(deserialize_with = "deserialize_zone")] + pub zone: ProviderZone, + pub arch: String, + #[serde(default)] + pub sshAccess: SSHAccess, + #[serde(default)] + pub networks: Vec, + #[serde(default,rename(deserialize = "storages"))] + pub storageDevices: Vec, + #[serde(default)] + pub timeZone: String, + #[serde(default)] + pub tags: Vec, + #[serde(default)] + pub useFloatIP: bool, + #[serde(default)] + pub userData: Option, + pub tsksrvcs: Vec, + pub apps: Vec, + pub clients: Vec, + pub rules: Vec, + pub backup: Vec, +} + +fn deserialize_plan<'de, D>(deserializer: D) -> Result +where D: Deserializer<'de> { + let buf = String::deserialize(deserializer)?; + // let res = String::from_str(&buf).map_err(serde::de::Error::custom) { + // .unwrap_or_else(|| serde::de::Error::custom); + Ok(ProviderServerPlan::set_plan(buf)) +} + +fn deserialize_zone<'de, D>(deserializer: D) -> Result +where D: Deserializer<'de> { + let buf = String::deserialize(deserializer)?; + Ok(ProviderZone::set_zone(buf)) +} + +impl Default for ServerInstance { + fn default() -> Self { + let net: NetworkInterface = NetworkInterface::default(); + let net_basic: NetworkInterface = NetworkInterface::upcloud_basic(); + let storage = StorageDevice::upcloud_storage_device_ubuntu20(25); + Self { + cluster: Cluster::default(), + hostname: String::from(""), + title: String::from(""), + loginUser: LoginUser::default(), + metadata: 1, + plan: ProviderServerPlan::upcloud_1xCPU_1GB, + zone: ProviderZone::upcloud_nl_ams1, + arch: String::from("linux_amd64"), + sshAccess: SSHAccess::default(), + networks: vec![ net, net_basic ], + storageDevices: vec![ storage ], + timeZone: String::from("UTC"), + tags: Vec::new(), + useFloatIP: false, + userData: Some(String::from("")), + tsksrvcs: Vec::new(), + apps: Vec::new(), + clients: Vec::new(), + rules: Vec::new(), + backup: Vec::new(), + } + } +} + +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SrvrCfg { + #[serde(default)] + pub role: String, + pub sshKeys: Vec, + #[serde(default)] + pub sshKeyPath: String, + pub storageDeviceSize: u16, + #[serde(default)] + pub tags: Vec, + #[serde(default)] + pub useFloatIP: bool +} +impl Default for SrvrCfg { + fn default() -> Self { + Self { + role: String::from(""), + sshKeys: Vec::new(), + sshKeyPath: String::from("/root/.ssh/id_rsa"), + storageDeviceSize: 25, + tags: Vec::new(), + useFloatIP: false + } + } +} + +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct ResourcesConfig { + pub floatIP: Option, + #[serde(default)] + pub hostPrefix: String, + #[serde(default)] + pub mainName: String, + pub group: String, + pub group_path: String, + pub provider: Option, + pub domainName: Option, + pub cntrllrs: Vec, + #[serde(default)] + pub srvrcfg: SrvrCfg, + #[serde(default)] + pub servers: Vec, +} + +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct ConfigResources { + pub floatIP: Option, + #[serde(default)] + pub hostPrefix: String, + #[serde(default)] + pub mainName: String, + pub group: String, + pub group_path: String, + pub provider: Option, + pub domainName: Option, + pub cntrllrs: Vec, + #[serde(default)] + pub servers: Vec, +} + +// Implementations & settings + + +impl StorageDevice { + pub fn upcloud_storage_device_ubuntu20 (size: u16) -> Self { + Self { + action: String::from("clone"), + storage: String::from("01000000-0000-4000-8000-000030200200"), + size: size, + finalSize: size, + partSizes: Some(String::from("")), + makefs: Some(String::from("")), + title: String::from("Ubuntu Server 20.04 LTS (Focal Fossa)"), + tier: String::from("maxiopsa"), + source: String::from(""), + } + } +} + +impl NetworkInterface { + pub fn upcloud_basic () -> Self { + let mut basic = NetworkInterface::default(); + basic.access = String::from("utility"); + basic + } + pub fn upcloud_sdn () -> Self { + Self { + access: String::from("private"), + family: String::from("IPv4"), + network: Some(String::from("")), + ipaddress: Some(String::from("")), + } + } +} + +impl ServerInstance { + pub fn server (plan: ProviderServerPlan, zone: ProviderZone, size: u16, arch: &str) -> Self { + let net: NetworkInterface = NetworkInterface::default(); + let net_basic: NetworkInterface = NetworkInterface::upcloud_basic(); + let storage = StorageDevice::upcloud_storage_device_ubuntu20(size); + Self { + cluster: Cluster::default(), + hostname: String::from(""), + title: String::from(""), + loginUser: LoginUser::default(), + metadata: 1, + plan, + zone, + arch: String::from(arch), + sshAccess: SSHAccess::default(), + networks: vec![ net, net_basic ], + storageDevices: vec![ storage ], + timeZone: String::from("UTC"), + tags: Vec::new(), + useFloatIP: false, + userData: Some(String::from("")), + tsksrvcs: Vec::new(), + apps: Vec::new(), + clients: Vec::new(), + rules: Vec::new(), + backup: Vec::new(), + } + } +} diff --git a/src/providers/manual.rs b/src/providers/manual.rs new file mode 100644 index 0000000..678fafb --- /dev/null +++ b/src/providers/manual.rs @@ -0,0 +1,96 @@ +use crate::defs::*; +use crate::clouds::defs::{ + Cloud, + MainResourcesConfig, +}; +use crate::tsksrvcs::{run_tsksrvc}; +use crate::providers::defs::manual::{ + ResourcesConfig, + ConfigResources, +}; + +use anyhow::{Result}; + +pub async fn parse_resources_cfg(res_cfg: &mut ResourcesConfig) -> Result<()> { + for i in 0..res_cfg.servers.len() { + if res_cfg.servers[i].cluster.role.len() == 0 { + res_cfg.servers[i].cluster.role = res_cfg.srvrcfg.role.to_owned(); + } + if res_cfg.servers[i].networks.len() == 0 { + res_cfg.servers[i].networks.push(NetworkInterface::default()); + res_cfg.servers[i].networks.push(NetworkInterface::manual_basic()); + } + if res_cfg.servers[i].storageDevices.len() == 0 { + let size = res_cfg.srvrcfg.storageDeviceSize; + let storage = StorageDevice::manual_storage_device_ubuntu20(size); + res_cfg.servers[i].storageDevices = vec![storage]; + } + if res_cfg.servers[i].loginUser.sshKeys.len() == 0 { + res_cfg.servers[i].loginUser.sshKeys = res_cfg.srvrcfg.sshKeys.to_owned(); + } + if res_cfg.servers[i].sshAccess.keyPath.len() == 0 { + res_cfg.servers[i].sshAccess.keyPath = res_cfg.srvrcfg.sshKeyPath.to_owned(); + } + if res_cfg.servers[i].sshAccess.host.len() == 0 { + res_cfg.servers[i].sshAccess.host = res_cfg.servers[i].hostname.to_owned(); + } + if res_cfg.servers[i].sshAccess.user.len() == 0 { + res_cfg.servers[i].sshAccess.user = String::from("root"); + } + if res_cfg.servers[i].tags.len() == 0 { + res_cfg.servers[i].tags = res_cfg.srvrcfg.tags.to_owned(); + } + res_cfg.servers[i].metadata = true; + } + + Ok(()) +} +pub async fn make_config_resources(res_cfg: &mut ResourcesConfig) -> Result { + Ok(ConfigResources { + floatIP: res_cfg.floatIP.to_owned(), + hostPrefix: res_cfg.hostPrefix.to_owned(), + mainName: res_cfg.mainName.to_owned(), + group: res_cfg.group.to_owned(), + group_path: res_cfg.group_path.to_owned(), + provider: res_cfg.provider.to_owned(), + domainName: res_cfg.domainName.to_owned(), + cntrllrs: res_cfg.cntrllrs.to_owned(), + servers: res_cfg.servers.to_owned(), + }) +} + +pub async fn run_tsksrvcs_list(cfg_data: &str, cloud: &Cloud, tsksrvc: &str, cmd: &str, nxt: &str, cfg: &MainResourcesConfig) -> Result<()> { + let mut res_cfg: ResourcesConfig = serde_yaml::from_str(&cfg_data)?; // .with_context(|| format!("Failed to parse 'cfg_path' from {}", &cfg_path))?; + // println!("{:#?}",&res_cfg); + parse_resources_cfg(&mut res_cfg).await?; + let config_res: ConfigResources = make_config_resources(&mut res_cfg).await?; + // println!("{:#?}",&)config_res; + // let str_config_res = serde_json::to_string(&config_res).with_context(|| format!("Failed to stringify 'config_res' from {}", &cfg_path))?; + // println!("{}",&str_config_res); + for (_, elem) in config_res.servers.iter().enumerate() { + // dbg!(&elem); + if ! cloud.env.listhosts.is_empty() && ! cloud.env.listhosts.contains(&elem.hostname) { + continue; + } + let mut use_next = false; + match tsksrvc { + "createserver" | "modifyip" | "startserver" => { + println!("TskSrvc {} ",&tsksrvc); + }, + _ => { + for (_, tsk) in elem.tsksrvcs.iter().enumerate() { + let tsk_name = format!("{}",&tsk.name); + if tsk_name.as_str() == tsksrvc && nxt == "next" { + use_next=true; + } + if tsksrvc == "all" || tsk_name.as_str() == tsksrvc || use_next { + let srvr_json = serde_yaml::to_string(elem)?; + let ssh_access: SSHAccess = elem.sshAccess.to_owned(); + run_tsksrvc(&cloud, &tsk, cmd, &elem.hostname, &cfg.domainName, srvr_json, ssh_access, &config_res.cntrllrs).await?; + } + } + } + }; + }; + Ok(()) +} diff --git a/src/providers/upcloud.rs b/src/providers/upcloud.rs new file mode 100644 index 0000000..e0890ca --- /dev/null +++ b/src/providers/upcloud.rs @@ -0,0 +1,96 @@ +use anyhow::{Result}; +// use std::str::FromStr; + +use crate::defs::*; + +use crate::clouds::defs::{ + Cloud, + MainResourcesConfig, +}; +use crate::tsksrvcs::{run_tsksrvc}; +use crate::providers::defs::upcloud::{ + ResourcesConfig, + ConfigResources, +}; + +pub async fn parse_resources_cfg(res_cfg: &mut ResourcesConfig) -> Result<()> { + for i in 0..res_cfg.servers.len() { + if res_cfg.servers[i].cluster.role.len() == 0 { + res_cfg.servers[i].cluster.role = res_cfg.srvrcfg.role.to_owned(); + } + if res_cfg.servers[i].networks.len() == 0 { + res_cfg.servers[i].networks.push(NetworkInterface::default()); + res_cfg.servers[i].networks.push(NetworkInterface::upcloud_basic()); + } + if res_cfg.servers[i].storageDevices.len() == 0 { + let size = res_cfg.srvrcfg.storageDeviceSize; + let storage = StorageDevice::upcloud_storage_device_ubuntu20(size); + res_cfg.servers[i].storageDevices = vec![storage]; + } + if res_cfg.servers[i].loginUser.sshKeys.len() == 0 { + res_cfg.servers[i].loginUser.sshKeys = res_cfg.srvrcfg.sshKeys.to_owned(); + } + if res_cfg.servers[i].sshAccess.keyPath.len() == 0 { + res_cfg.servers[i].sshAccess.keyPath = res_cfg.srvrcfg.sshKeyPath.to_owned(); + } + if res_cfg.servers[i].sshAccess.host.len() == 0 { + res_cfg.servers[i].sshAccess.host = res_cfg.servers[i].hostname.to_owned(); + } + if res_cfg.servers[i].sshAccess.user.len() == 0 { + res_cfg.servers[i].sshAccess.user = String::from("root"); + } + if res_cfg.servers[i].tags.len() == 0 { + res_cfg.servers[i].tags = res_cfg.srvrcfg.tags.to_owned(); + } + res_cfg.servers[i].metadata = 1; + } + Ok(()) +} +pub async fn make_config_resources(res_cfg: &mut ResourcesConfig) -> Result { + Ok(ConfigResources { + floatIP: res_cfg.floatIP.to_owned(), + hostPrefix: res_cfg.hostPrefix.to_owned(), + mainName: res_cfg.mainName.to_owned(), + group: res_cfg.group.to_owned(), + group_path: res_cfg.group_path.to_owned(), + provider: res_cfg.provider.to_owned(), + domainName: res_cfg.domainName.to_owned(), + cntrllrs: res_cfg.cntrllrs.to_owned(), + servers: res_cfg.servers.to_owned(), + }) +} +pub async fn run_tsksrvcs_list(cfg_data: &str, cloud: &Cloud, tsksrvc: &str, cmd: &str, nxt: &str, cfg: &MainResourcesConfig) -> Result<()> { + let mut res_cfg: ResourcesConfig = serde_yaml::from_str(&cfg_data)?; // .with_context(|| format!("Failed to parse 'cfg_path' from {}", &cfg_path))?; + // println!("{:#?}",&res_cfg); + parse_resources_cfg(&mut res_cfg).await?; + let config_res: ConfigResources = make_config_resources(&mut res_cfg).await?; + // println!("{:#?}",&config_res); + // let str_config_res = serde_json::to_string(&config_res).with_context(|| format!("Failed to stringify 'config_res' from {}", &cfg_path))?; + // println!("{}",&str_config_res); + for (_, elem) in config_res.servers.iter().enumerate() { + // dbg!(&elem); + if ! cloud.env.listhosts.is_empty() && ! cloud.env.listhosts.contains(&elem.hostname) { + continue; + } + let mut use_next = false; + match tsksrvc { + "createserver" | "modifyip" | "startserver" => { + println!("TskSrvc {} ",&tsksrvc); + }, + _ => { + for (_, tsk) in elem.tsksrvcs.iter().enumerate() { + let tsk_name = format!("{}",&tsk.name); + if tsk_name.as_str() == tsksrvc && nxt == "next" { + use_next=true; + } + if tsksrvc == "all" || tsk_name.as_str() == tsksrvc || use_next { + let srvr_json = serde_yaml::to_string(elem)?; + let ssh_access: SSHAccess = elem.sshAccess.to_owned(); + run_tsksrvc(&cloud, &tsk, cmd, &elem.hostname, &cfg.domainName, srvr_json, ssh_access, &config_res.cntrllrs).await?; + } + } + } + }; + }; + Ok(()) +} \ No newline at end of file diff --git a/src/tsksrvcs.rs b/src/tsksrvcs.rs new file mode 100644 index 0000000..4b425f8 --- /dev/null +++ b/src/tsksrvcs.rs @@ -0,0 +1,308 @@ +use std::collections::HashMap; +use rfm::mkdir; +use anyhow::{anyhow,Result,Context}; +use std::fs; //, io}; +// use std::os::unix::fs::PermissionsExt; +use std::fs::OpenOptions; +use std::io::{Write}; +use std::path::Path; +use flate2::Compression; +use flate2::write::GzEncoder; +use std::process::{Command}; + +use crate::pkgs::{load_pack_config}; +use crate::providers::{upcloud, manual}; +// use crate::defs::TskSrvc; +// use crate::cmds::ssh; +// use tempfile::tempfile; + +use crate::utils::{parse_yaml_value, write_log, liveness_check}; // host_ssh_is_alive + +use crate::defs::{SSHAccess,Cntrllr}; +use crate::clouds::defs::{ + ConfigTskSrvc, + Provider, + TskSrvc, + MainResourcesConfig, + Cloud +}; + +pub async fn create_tsksrvc_files(cloud: &Cloud, tsk: &TskSrvc, files: HashMap, run_pack: &str,tsksrvc_vars: &HashMap,tsksrvc_settings: &HashMap) -> Result<()> { + let mut tera = tera::Tera::default(); + let mut context = tera::Context::new(); + for (key, value) in tsksrvc_vars { + if key == "srvr" { + let srvr_data:serde_yaml::Value = serde_yaml::from_str(&value)?; + parse_yaml_value(srvr_data,key.to_owned(),&mut context); + } else { + context.insert(key.to_owned(),&value); + } + } + for (key, value) in tsksrvc_settings { + parse_yaml_value(value.to_owned(),key.to_owned(),&mut context); + } + // dbg!(&context); + + for (template_name, value) in files { + if template_name.starts_with("#") { + continue; + } + let mut output_root_path=format!("{}/{}",&cloud.env.provision,&tsk.name); + if template_name == run_pack { + output_root_path = format!("{}",&cloud.env.wk_path); + } + let template_path=format!("{}/{}/{}",&cloud.env.tsksrvcs_path,&tsk.name,&template_name); + let output_path=format!("{}/{}",&output_root_path,template_name.replace(".j2","")); + // if !Path::new(&output_root_path).exists() { + // fs::create_dir(&output_root_path)?; + // } + // dbg!(&template_path); + // dbg!(&output_path); + let out_path = Path::new(&output_path); + if let Some(dir_path) = out_path.parent() { + if !dir_path.exists() { + let dir_path_buf = dir_path.to_path_buf(); + let dirs: Vec<&std::path::PathBuf> = vec![&dir_path_buf]; + mkdir(&dirs) + .with_context(|| format!("\nFailed to create dir path {}", &output_path))?; + println!("{} created path for ", &output_path); + // fs::create_dir(&dir_path)?; + } + } + print!(" {} ",&template_name); + // if value.to_string().contains("tpl") { + match value.as_str() { + "tpl" => { + let template = fs::read_to_string(&template_path) + .with_context(|| format!("\nFailed to read template from {}", &template_path))?; + let append = value.to_string().contains("append"); + tera.add_raw_templates(vec![(template_name.to_owned(), &template)]) + .with_context(|| format!("Failed to add template {}", &template_name))?; + // dbg!(&context); + let out=tera + .render(&template_name, &context) + .with_context(|| format!("\nFailed to render {}", &template_path))?; + //println!("{}",&output_path); + if append && Path::new(&output_path).exists() { + let mut file = OpenOptions::new().append(true).open(&output_path)?; + // out = out.replace("/", "/"); + file.write(out.as_bytes())?; + } else { + if Path::new(&output_path).exists() { + fs::remove_file(&output_path)?; + } + let mut file = OpenOptions::new().write(true).create(true).open(&output_path) + .with_context(|| format!("Failed to write template {}", &output_path))?; + file.write_all(out.as_bytes())?; + if output_path.ends_with(".sh") { + // println!("\nSetting permissions for {}",&output_path); + // let file_sh = fs::File::open(&output_path)?; + // let metadata = file_sh.metadata()?; + // let mut permissions = metadata.permissions(); + // permissions.set_mode(0o755); + Command::new("chmod") + .arg("+x") + .arg(&output_path) + .output()?; + } + } + }, + "cp" => { + fs::copy(&template_path, &output_path)?; + }, + _ => { print!("?"); }, + } + }; + println!(""); + Ok(()) +} +/// ``` + +pub async fn run_cmd_shell(cmd_name: &str,cloud: &Cloud, cmd: &str, hostname: &str, target_path: String, tsk_name: &str) -> Result<()> { + // println!("{} {} ..",&cmd_name,&cloud.env.source_path); + // println!("{} {} ..",&tsk_name,&target_path); + let output = Command::new(&target_path) + .arg(&cloud.env.source) + .arg(&tsk_name) + .arg(&cmd) + .arg(&hostname) + .output()?; + // dbg!(&output); + if !&output.status.success() { + return Err(anyhow!("run '{}' failed: {}",&target_path,&output.status)); + } + println!("{} {}: {}",&cmd_name,&cloud.env.source_path,&output.status); + write_log(format!("{}/log",&cloud.env.provision).as_str(), &hostname, &tsk_name, format!("{} {} done",&cmd_name,&target_path).as_str()).await?; + Ok(()) +} +pub async fn run_pack_shell(cloud: &Cloud, tsk: &TskSrvc, cmd: &str, hostname: &str, tsk_name: &str, run_pack_path: String) -> Result<()> { + let output_root_path=format!("{}/{}",&cloud.env.provision,&tsk.name); + let archive_file=format!("{}/{}_{}.tar.gz",&cloud.env.wk_path,&hostname,&tsk.name); + let tar_gz = fs::File::create(&archive_file)?; + let enc = GzEncoder::new(tar_gz, Compression::default()); + let mut tar = tar::Builder::new(enc); + tar.append_dir_all(&tsk_name, &output_root_path)?; + tar.into_inner()?; + let output = Command::new("bash") + .arg(&run_pack_path) + .arg(&cmd) + .output()?; + // dbg!(&output); + if !&output.status.success() { + return Err(anyhow!("run '{}' on '{}' failed: {}",&run_pack_path,&tsk.name,&output.status)); + } + println!("run {}: {}",&tsk.name,&output.status); + write_log(format!("{}/log",&cloud.env.provision).as_str(), &hostname, &tsk_name, "run").await?; + Ok(()) +} +pub fn tsksrvc_vars_into_settings(root_path: &str, source_path: String, tsk_name: &str, settings: &mut HashMap) -> Result<()> { + let tsksrvc_data= fs::read_to_string(&source_path) + .with_context(|| format!("Failed to read 'tsksrvcs_path' from {}", &source_path))?; + let tsksrvc_settings: HashMap = serde_yaml::from_str(&tsksrvc_data)?; + for (key, value) in tsksrvc_settings { + match key.as_str() { + "include" => + if let Some(val) = value.as_str() { + for req_tsksrvc in val.split(" ") { + let include_tsksrvc_path=format!("{}/{}.yaml",&root_path,&req_tsksrvc); + if Path::new(&include_tsksrvc_path).exists() { + tsksrvc_vars_into_settings(root_path,include_tsksrvc_path,tsk_name,settings)?; + } + } + } + , + "pause" => { run_pause(tsk_name, settings); }, + _ => { settings.insert(key, value); } + }; + } + Ok(()) +} +pub fn run_pause(tsk_name: &str, settings: &mut HashMap) { + let mut num_time: u64 = 8; // as default + if let Some(time_to_sleep) = settings.get("sleep_time") { + if let Some(val) = time_to_sleep.as_u64() { + num_time = val; + } + } + let sleep_time = std::time::Duration::new(num_time, 0); + println!("Waiting {} tsksrvc for {} seconds ...",&tsk_name,&num_time); + std::thread::sleep(sleep_time); +} +/// `run_taks` prepare and run a __tsksrvc__ in cloud configuration in a __hostname__ target +/// +/// 1) Create a HashMap with __pack_config__ settings for __tsk.name__ +/// 2) Create __provision path__ for target cloud +/// 3) Load __config.yaml__ for target __pack__ (__tsk.name__) +/// 4) Create a `Tera` default and context with pack and env settings +/// 5) for (template_name, value) in konfig parse target +/// 6) Render template if value is 'tpl' of copy file if is 'cp' +/// 7) Pack all files into a '.tar.gz' +/// 8) run tsksrvc_`tsk.name`_run.sh ( to scp '.tar.gz', run 'install.sh' and clean tsksrvc files +/// +/// # Examples +/// +/// ```no_run +/// ``` +pub async fn run_tsksrvc(cloud: &Cloud, tsk: &TskSrvc, cmd: &str, hostname: &str, domain_name: &str, srvr: String, ssh_access: SSHAccess, cntrllrs: &Vec) -> Result<()> { + // load tsksrvc status + // if "done" return + // host has connection ? + let tsk_name = format!("{}",&tsk.name); + let serverstring = format!("{}:{}",&ssh_access.host,&ssh_access.port); + liveness_check(&cloud.env.source, &cntrllrs , &serverstring, &tsk_name).await?; + // host_ssh_is_alive(cloud, cmd, hostname, &tsk_name, &ssh_access).await?; + let prepare_source_path = format!("{}/prepare.sh",&cloud.env.source_path); + if Path::new(&prepare_source_path).exists() { + run_cmd_shell("prepare",cloud, cmd, hostname, prepare_source_path, &tsk_name).await?; + } + let prepare_path = format!("{}/{}/prepare.sh",&cloud.env.tsksrvcs_path,&tsk.name); + if Path::new(&prepare_path).exists() { + run_cmd_shell("prepare",cloud, cmd, hostname, prepare_path, &tsk_name).await?; + } + // dbg!(&cloud); + let mut tsksrvc_vars: HashMap = HashMap::new(); + tsksrvc_vars.insert("source".to_string(), cloud.env.source.to_owned()); + tsksrvc_vars.insert("hostname".to_string(), hostname.to_owned()); + tsksrvc_vars.insert("domainName".to_string(), domain_name.to_owned()); + tsksrvc_vars.insert("command".to_string(), cmd.to_owned()); + tsksrvc_vars.insert("srvr".to_string(), srvr.to_owned()); + tsksrvc_vars.insert("tsksrvcname".to_string(), tsk_name.to_owned()); + load_pack_config(&mut tsksrvc_vars, cloud, &tsk, &hostname)?; + // dbg!(&tsksrvc_vars); + + // create path + if !Path::new(&cloud.env.provision).exists() { + fs::create_dir(&cloud.env.provision)?; + } + let run_pack = "run.sh.j2"; + + // tsksrvc vars + let mut tsksrvc_settings: HashMap = HashMap::new(); + let tsksrvc_root_path=format!("{}/tsksrvcs",&cloud.env.source_path); + + let global_tsksrvc_path=format!("{}/global.yaml",&tsksrvc_root_path); + if Path::new(&global_tsksrvc_path).exists() { + tsksrvc_vars_into_settings(&tsksrvc_root_path,global_tsksrvc_path,&tsk_name,&mut tsksrvc_settings)?; + } + let tsksrvc_path=format!("{}/{}.yaml",&tsksrvc_root_path,&tsk.name); + if Path::new(&tsksrvc_path).exists() { + tsksrvc_vars_into_settings(&tsksrvc_root_path,tsksrvc_path,&tsk_name,&mut tsksrvc_settings)?; + } + let host_tsksrvc_path=format!("{}/{}_{}.yaml",&tsksrvc_root_path,&hostname,&tsk.name); + if Path::new(&host_tsksrvc_path).exists() { + tsksrvc_vars_into_settings(&tsksrvc_root_path,host_tsksrvc_path,&tsk_name,&mut tsksrvc_settings)?; + } + // dbg!(&tsksrvc_settings); + if tsk_name.as_str() == "pause" { + run_pause(&tsk_name, &mut tsksrvc_settings); + } + // tsksrvc file config list: + let tsksrvc_config_path=format!("{}/{}/config.yaml",&cloud.env.tsksrvcs_path,&tsk.name); + if Path::new(&tsksrvc_config_path).exists() { + let tsksrvc_config_data= fs::read_to_string(&tsksrvc_config_path) + .with_context(|| format!("Failed to read 'tsksrvcs_path' from {}", &tsksrvc_config_path))?; + let tsksrvc_config: ConfigTskSrvc = serde_yaml::from_str(&tsksrvc_config_data)?; + create_tsksrvc_files(cloud, tsk, tsksrvc_config.files, &run_pack, &tsksrvc_vars, &tsksrvc_settings).await?; + } + // local cloud tsksrvcs config list: + let local_tsksrvc_config_path=format!("{}/tsksrvcs/config_{}.yaml",&cloud.env.source_path,&tsk.name); + if Path::new(&local_tsksrvc_config_path).exists() { + let local_tsksrvc_config_data= fs::read_to_string(&local_tsksrvc_config_path) + .with_context(|| format!("Failed to read 'tsksrvcs_path' from {}", &local_tsksrvc_config_path))?; + let local_tsksrvc_config: ConfigTskSrvc = serde_yaml::from_str(&local_tsksrvc_config_data)?; + create_tsksrvc_files(cloud, tsk, local_tsksrvc_config.files, &run_pack, &tsksrvc_vars, &tsksrvc_settings).await?; + } + // tsksrvc run_pack + let run_pack_path=format!("{}/{}",&cloud.env.wk_path,run_pack.replace(".j2","")); + if Path::new(&run_pack_path).exists() { + run_pack_shell(cloud, tsk, cmd, hostname, &tsk_name, run_pack_path).await?; + } + // tsksrvc postinstall + let postinstall_path = format!("{}/{}/postinstall.sh",&cloud.env.tsksrvcs_path,&tsk.name); + if Path::new(&postinstall_path).exists() { + run_cmd_shell("postinstall",cloud, cmd, hostname, postinstall_path, &tsk_name).await?; + } + // source postinstall + let postinstall_source_path = format!("{}/postinstall.sh",&cloud.env.source_path); + if Path::new(&postinstall_source_path).exists() { + run_cmd_shell("postinstall",cloud, cmd, hostname, postinstall_source_path, &tsk_name).await?; + } + write_log(format!("{}/log",&cloud.env.provision).as_str(), &hostname, &tsk_name, "done").await?; + Ok(()) +} + +pub async fn run_tsksrvcs_on_providers(provider: &Provider, cfg_data: &str, cloud: &Cloud, tsksrvc: &str, cmd: &str, nxt: &str, cfg: &MainResourcesConfig ) -> Result<()> { + match provider.name.as_str() { + "upcloud" => { + upcloud::run_tsksrvcs_list(cfg_data, cloud, tsksrvc, cmd, &nxt, cfg).await?; + Ok(()) + }, + "manual" => { + manual::run_tsksrvcs_list(cfg_data, cloud, tsksrvc, cmd, &nxt, cfg).await?; + println!("Provider '{}' create manually",&provider.name); + Ok(()) + }, + _ => + Err(anyhow!("Provider '{}' undefined",&provider.name)), + } +} \ No newline at end of file diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..e515ddb --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,146 @@ +use anyhow::{anyhow,Result}; +use std::str; +use std::fs; //, io}; +use std::fs::OpenOptions; +use std::io::{Write}; +// use std::io::prelude::*; +use std::net::TcpStream; +use std::path::Path; +use std::process::{Command}; + +use crate::defs::{SSHAccess,Cntrllr}; +use crate::clouds::defs::{Cloud}; + +pub fn parse_yaml_value(value: serde_yaml::Value, key: String, ctx: &mut tera::Context ) { + if let Some(v) = value.as_str() { + ctx.insert(key.to_owned(),&v); + } else if let Some(v) = value.as_bool() { + ctx.insert(key.to_owned(),&v); + } else if value.is_mapping() { + if let Some(map) = value.as_mapping() { + ctx.insert(key.to_owned(),&map); + } + } else if value.is_sequence() { + if let Some(seq) = value.as_sequence() { + // let mut data_seq: Vec = Vec::new(); + // for (line, elem) in seq.iter().enumerate() { + // if let Some(s) = elem.as_str() { + // data_seq.push(s.to_owned()); + // } else { + // println!("Error {} line {}",&key,&line); + // } + // } + // ctx.insert(key.to_owned(),&data_seq); + ctx.insert(key.to_owned(),&seq); + } + } else { + ctx.insert(key.to_owned(),&value); + } +} +pub async fn write_log(output_path: &str, hostname: &str, name: &str, msg: &str) -> Result<()> { + let now = chrono::Utc::now().timestamp(); + let out = format!("{}: [{}] {} -> {}\n",&now,&hostname, &name,&msg); + if Path::new(&output_path).exists() { + let mut file = OpenOptions::new().append(true).open(&output_path)?; + // out = out.replace("/", "/"); + file.write(out.as_bytes())?; + } else { + if Path::new(&output_path).exists() { + fs::remove_file(&output_path)?; + } + let mut file = OpenOptions::new().write(true).create(true).open(&output_path)?; + file.write_all(out.as_bytes())?; + } + Ok(()) +} + +pub fn source_host() -> String { + // TODO a better way by finding cntrllr in config definition + let running_host = envmnt::get_or("HOSTNAME", "localhost"); + let cntrllr_host = envmnt::get_or("CNTRLLR_HOST", ""); + if running_host == cntrllr_host { + running_host.to_owned() + } else { + cntrllr_host.to_owned() + } +} +pub async fn liveness_check(source: &str,cntrllrs: &Vec,serverstring: &str,tsk_name: &str) -> Result<()> { + let debug=envmnt::get_isize("DEBUG",0); + // let serverstring = format!("{}:22",&hostname); + let mut check_cntrllr = Cntrllr::default(); + for cntrllr in cntrllrs { + let serverstring = format!("{}:{}",&cntrllr.sshaccess.host,&cntrllr.sshaccess.port); + match TcpStream::connect(&serverstring) { + Ok(_serverstream) => { + check_cntrllr = cntrllr.to_owned(); + break; + // handle_input(serverstream); + }, + Err(e) => { + println!("Error: {}:{} -> {}",&cntrllr.sshaccess.host,&cntrllr.sshaccess.port,e); + } + } + } + // Some Ips in serverstring can be private so we need to know from where are we checking ... + if !check_cntrllr.sshaccess.host.is_empty() && envmnt::get_or("HOSTNAME", "localhost") == check_cntrllr.sshaccess.host { + if debug > 1 { + println!("Checking connection to {} for {} -> {}",&serverstring,&source,&tsk_name); + } + match TcpStream::connect(&serverstring) { + Ok(_serverstream) => { + Ok(()) + // handle_input(serverstream); + }, + Err(e) => { + Err(anyhow!("Source {}: Connection to '{}' for tsksrvc '{}' failed: {}",&source,&serverstring,&tsk_name,&e)) + } + } + } else { + if debug > 1 { + println!("Remote checking connection from {} to {} for {} -> {}",&check_cntrllr.sshaccess.host,&serverstring,&source,&tsk_name); + } + let vec_serverstring: Vec<&str> = serverstring.split(':').collect(); + let output = Command::new("ssh") + .arg("-o") + .arg("StrictHostKeyChecking=accept-new") + .arg("-p") + .arg(format!("{}",check_cntrllr.sshaccess.port)) + .arg(format!("{}@{}",check_cntrllr.sshaccess.user,&check_cntrllr.sshaccess.host)) + .arg("nc") + .arg("-zv") + .arg(format!("{}",vec_serverstring[0])) + .arg(format!("{}",vec_serverstring[1])) + .output()?; + match &output.status.code() { + Some(code) => + if format!("{}",code) == "0" { + Ok(()) + } else { + let err = str::from_utf8(&output.stderr).unwrap_or_else(|_| ""); + Err(anyhow!("Source {}: Connection to '{}' ({}) for tsksrvc '{}' failed:\n {}",&source,serverstring,&check_cntrllr.sshaccess.host,&tsk_name,&err)) + } + None => Ok(()) + } + } +} + +pub async fn host_ssh_is_alive(cloud: &Cloud, cmd: &str, hostname: &str,tsk_name: &str, ssh_access: &SSHAccess) -> Result<()> { + let debug=envmnt::get_isize("DEBUG",0); + if debug > 1 { + println!("Checking ssh connection to {}@{} on {} for {} -> {}",ssh_access.user,ssh_access.host,ssh_access.port,&cloud.env.source,&tsk_name); + } + let output = Command::new("ssh") + .arg("-o") + .arg("StrictHostKeyChecking=accept-new") + .arg("-p") + .arg(format!("{}",ssh_access.port)) + .arg(format!("{}@{}",ssh_access.user,ssh_access.host)) + .arg("ls") + .output()?; + // dbg!(&output); + if !&output.status.success() { + let err = str::from_utf8(&output.stderr).unwrap_or_else(|_| ""); + return Err(anyhow!("Source {}: Connection to '{}' for tsksrvc '{}' cmd '{}' failed: {}",&cloud.env.source,&hostname,&tsk_name,&cmd,&err)); + } + Ok(()) +} \ No newline at end of file