bsudlib/
config.rs

1use easy_error::format_err;
2use lazy_static::lazy_static;
3use log::debug;
4use outscale_api::apis::configuration::AWSv4Key;
5use secrecy::Secret;
6use secrecy::SecretString;
7use serde::Deserialize;
8use std::env;
9use std::error::Error;
10use std::fs::read_to_string;
11use std::str::FromStr;
12use std::sync::RwLock;
13
14type CloudConfig = outscale_api::apis::configuration::Configuration;
15
16const VERSION: &str = env!("CARGO_PKG_VERSION");
17const METADATA_SUBREGION_URL: &str =
18    "http://169.254.169.254/latest/meta-data/placement/availability-zone";
19const METADATA_VMID_URL: &str = "http://169.254.169.254/latest/meta-data/instance-id";
20
21lazy_static! {
22    pub static ref CLOUD_CONFIG: RwLock<CloudConfig> = RwLock::new(CloudConfig::new());
23    pub static ref REGION: RwLock<String> = RwLock::new(String::new());
24    pub static ref SUBREGION: RwLock<String> = RwLock::new(String::new());
25    pub static ref VM_ID: RwLock<String> = RwLock::new(String::new());
26}
27#[derive(Deserialize, Debug)]
28pub struct Config {
29    pub drives: Vec<ConfigFileDrive>,
30}
31
32pub fn discover_vm_config() -> Result<(), Box<dyn Error>> {
33    debug!("getting subregion from metadata");
34    let subregion = reqwest::blocking::get(METADATA_SUBREGION_URL)?.text()?;
35    let mut region = subregion.clone();
36    region.pop();
37    {
38        *SUBREGION.write()? = subregion;
39        *REGION.write()? = region;
40    }
41    debug!("get vm id");
42    let vm_id = reqwest::blocking::get(METADATA_VMID_URL)?.text()?;
43    {
44        *VM_ID.write()? = vm_id;
45    }
46    Ok(())
47}
48
49pub fn region() -> Result<String, Box<dyn Error>> {
50    Ok(String::from(&(*REGION.read()?)))
51}
52
53pub fn load(path: String) -> Result<Config, Box<dyn Error>> {
54    debug!("trying to read \"{}\"", path);
55    let data = read_to_string(path)?;
56    let config_file: ConfigFile = serde_json::from_str(&data)?;
57
58    let config_file_auth = match config_file.authentication {
59        Some(c) => c,
60        None => {
61            debug!("cannot get credentials through configuration file, trying to get credentials through env");
62            let Ok(access_key) = env::var("OSC_ACCESS_KEY") else {
63                return Err(Box::new(format_err!(
64                    "Cannot get OSC_ACCESS_KEY env variable"
65                )));
66            };
67            let Ok(secret_key) = env::var("OSC_SECRET_KEY") else {
68                return Err(Box::new(format_err!(
69                    "Cannot get OSC_SECRET_KEY env variable"
70                )));
71            };
72            ConfigFileAuth {
73                access_key,
74                secret_key: SecretString::new(secret_key),
75            }
76        }
77    };
78    discover_vm_config()?;
79
80    debug!("forge cloud configuration");
81    let mut cloud_config = CloudConfig::new();
82    let region = region()?;
83    cloud_config.aws_v4_key = Some(AWSv4Key {
84        access_key: config_file_auth.access_key,
85        secret_key: config_file_auth.secret_key,
86        region: region.clone(),
87        service: "oapi".to_string(),
88    });
89    cloud_config.user_agent = Some(format!("bsud/{}", VERSION));
90    cloud_config.base_path = format!("https://api.{}.outscale.com/api/v1", region);
91    {
92        *CLOUD_CONFIG.write()? = cloud_config;
93    }
94
95    Ok(Config {
96        drives: config_file.drives,
97    })
98}
99
100#[derive(Deserialize, Debug)]
101struct ConfigFile {
102    authentication: Option<ConfigFileAuth>,
103    drives: Vec<ConfigFileDrive>,
104}
105
106#[derive(Deserialize, Debug)]
107#[serde(rename_all = "kebab-case")]
108pub struct ConfigFileAuth {
109    access_key: String,
110    secret_key: Secret<String>,
111}
112
113#[derive(Deserialize, Debug, Clone)]
114#[serde(rename_all = "kebab-case")]
115pub struct ConfigFileDrive {
116    pub name: String,
117    pub target: DriveTarget,
118    pub mount_path: String,
119    pub disk_type: Option<DiskType>,
120    pub disk_iops_per_gib: Option<usize>,
121    pub max_total_size_gib: Option<usize>,
122    pub initial_size_gib: Option<usize>,
123    pub max_bsu_count: Option<usize>,
124    pub max_used_space_perc: Option<usize>,
125    pub min_used_space_perc: Option<usize>,
126    pub disk_scale_factor_perc: Option<usize>,
127}
128
129#[derive(Deserialize, Debug, Clone)]
130#[serde(rename_all = "kebab-case")]
131pub enum DriveTarget {
132    Online,  // normal  drive flow, drive is available
133    Offline, // unmount + detach from VM
134    Delete,  // unmount + detach from VM + delete data
135}
136
137impl FromStr for DriveTarget {
138    type Err = ();
139    fn from_str(input: &str) -> Result<DriveTarget, Self::Err> {
140        match input.to_lowercase().as_str() {
141            "online" => Ok(DriveTarget::Online),
142            "offline" => Ok(DriveTarget::Offline),
143            "delete" => Ok(DriveTarget::Delete),
144            _ => Err(()),
145        }
146    }
147}
148
149impl ToString for DriveTarget {
150    fn to_string(&self) -> String {
151        match self {
152            DriveTarget::Online => String::from("online"),
153            DriveTarget::Offline => String::from("offline"),
154            DriveTarget::Delete => String::from("delete"),
155        }
156    }
157}
158
159#[derive(Deserialize, Debug, Clone, PartialEq)]
160#[serde(rename_all = "kebab-case")]
161pub enum DiskType {
162    Standard,
163    Gp2,
164    Io1,
165}
166
167impl FromStr for DiskType {
168    type Err = ();
169    fn from_str(input: &str) -> Result<DiskType, Self::Err> {
170        match input.to_lowercase().as_str() {
171            "standard" => Ok(Self::Standard),
172            "gp2" => Ok(Self::Gp2),
173            "io1" => Ok(Self::Io1),
174            _ => Err(()),
175        }
176    }
177}
178
179impl ToString for DiskType {
180    fn to_string(&self) -> String {
181        match self {
182            Self::Standard => "standard".to_string(),
183            Self::Gp2 => "gp2".to_string(),
184            Self::Io1 => "io1".to_string(),
185        }
186    }
187}