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, Offline, Delete, }
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}