1use std::env;
24
25pub const DEFAULT_ENV: &str = "aws";
27
28#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
31pub enum RackDistance {
32 Same,
34 SameDc,
36 Remote,
38}
39
40impl RackDistance {
41 #[must_use]
50 pub fn cost(self) -> u8 {
51 match self {
52 RackDistance::Same => 0,
53 RackDistance::SameDc => 1,
54 RackDistance::Remote => 2,
55 }
56 }
57}
58
59#[must_use]
69pub fn rack_distance(
70 self_dc: &str,
71 self_rack: &str,
72 other_dc: &str,
73 other_rack: &str,
74) -> RackDistance {
75 if self_dc != other_dc {
76 RackDistance::Remote
77 } else if self_rack != other_rack {
78 RackDistance::SameDc
79 } else {
80 RackDistance::Same
81 }
82}
83
84#[must_use]
98pub fn pick_target_rack<'a>(
99 self_dc: &str,
100 self_rack: &str,
101 candidates: &'a [(&'a str, &'a str)],
102) -> Option<(&'a str, &'a str)> {
103 let mut best: Option<(RackDistance, (&str, &str))> = None;
104 for &(dc, rack) in candidates {
105 let d = rack_distance(self_dc, self_rack, dc, rack);
106 match best {
107 Some((bd, _)) if bd.cost() <= d.cost() => {}
108 _ => best = Some((d, (dc, rack))),
109 }
110 }
111 best.map(|(_, p)| p)
112}
113
114#[must_use]
125pub fn is_aws_env(env_label: &str) -> bool {
126 env_label.starts_with(DEFAULT_ENV)
127}
128
129pub fn broadcast_address(
146 env_label: &str,
147 peer_name_fallback: &str,
148 lookup_env: &mut dyn FnMut(&str) -> Option<String>,
149) -> String {
150 let key = if is_aws_env(env_label) {
151 "EC2_PUBLIC_HOSTNAME"
152 } else {
153 "PUBLIC_HOSTNAME"
154 };
155 if let Some(v) = lookup_env(key) {
156 return v;
157 }
158 peer_name_fallback.to_string()
159}
160
161pub fn public_hostname(
165 env_label: &str,
166 peer_name_fallback: &str,
167 lookup_env: &mut dyn FnMut(&str) -> Option<String>,
168) -> Option<String> {
169 let key = if is_aws_env(env_label) {
170 "EC2_PUBLIC_HOSTNAME"
171 } else {
172 "PUBLIC_HOSTNAME"
173 };
174 if let Some(v) = lookup_env(key) {
175 return Some(v);
176 }
177 let first = peer_name_fallback.bytes().next()?;
178 if first.is_ascii_digit() {
179 None
180 } else {
181 Some(peer_name_fallback.to_string())
182 }
183}
184
185pub fn public_ip4(
188 env_label: &str,
189 peer_name_fallback: &str,
190 lookup_env: &mut dyn FnMut(&str) -> Option<String>,
191) -> Option<String> {
192 let key = if is_aws_env(env_label) {
193 "EC2_PUBLIC_IPV4"
194 } else {
195 "PUBLIC_IPV4"
196 };
197 if let Some(v) = lookup_env(key) {
198 return Some(v);
199 }
200 let first = peer_name_fallback.bytes().next()?;
201 if first.is_ascii_digit() {
202 Some(peer_name_fallback.to_string())
203 } else {
204 None
205 }
206}
207
208pub fn private_ip4(
211 env_label: &str,
212 lookup_env: &mut dyn FnMut(&str) -> Option<String>,
213) -> Option<String> {
214 let key = if is_aws_env(env_label) {
215 "EC2_LOCAL_IPV4"
216 } else {
217 "LOCAL_IPV4"
218 };
219 lookup_env(key)
220}
221
222pub fn process_env_lookup() -> impl FnMut(&str) -> Option<String> {
235 |key: &str| env::var(key).ok()
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn distance_orders_correctly() {
244 assert_eq!(rack_distance("dc", "r", "dc", "r"), RackDistance::Same);
245 assert_eq!(rack_distance("dc", "r", "dc", "x"), RackDistance::SameDc);
246 assert_eq!(rack_distance("dc", "r", "dx", "r"), RackDistance::Remote);
247 }
248
249 #[test]
250 fn pick_target_rack_prefers_local_rack() {
251 let cands = [("dc", "r"), ("dc", "x"), ("d2", "r")];
252 let pick = pick_target_rack("dc", "r", &cands);
253 assert_eq!(pick, Some(("dc", "r")));
254 }
255
256 #[test]
257 fn pick_target_rack_falls_back_to_same_dc() {
258 let cands = [("dc", "x"), ("d2", "r")];
259 let pick = pick_target_rack("dc", "r", &cands);
260 assert_eq!(pick, Some(("dc", "x")));
261 }
262
263 #[test]
264 fn pick_target_rack_falls_back_to_remote() {
265 let cands = [("d2", "r")];
266 let pick = pick_target_rack("dc", "r", &cands);
267 assert_eq!(pick, Some(("d2", "r")));
268 }
269
270 #[test]
271 fn pick_target_rack_empty() {
272 let cands: [(&str, &str); 0] = [];
273 let pick = pick_target_rack("dc", "r", &cands);
274 assert!(pick.is_none());
275 }
276
277 #[test]
278 fn broadcast_uses_env_first() {
279 let mut envs = |k: &str| {
280 if k == "EC2_PUBLIC_HOSTNAME" {
281 Some("ec2-host".into())
282 } else {
283 None
284 }
285 };
286 assert_eq!(broadcast_address("aws", "fb", &mut envs), "ec2-host");
287 }
288
289 #[test]
290 fn broadcast_falls_back_to_peer_name() {
291 let mut envs = |_: &str| None;
292 assert_eq!(
293 broadcast_address("aws", "127.0.0.1", &mut envs),
294 "127.0.0.1"
295 );
296 }
297
298 #[test]
299 fn public_hostname_skips_numeric_fallback() {
300 let mut envs = |_: &str| None;
301 assert!(public_hostname("baremetal", "1.2.3.4", &mut envs).is_none());
302 assert_eq!(
303 public_hostname("baremetal", "host.dns", &mut envs).as_deref(),
304 Some("host.dns"),
305 );
306 }
307
308 #[test]
309 fn public_ip4_skips_dns_fallback() {
310 let mut envs = |_: &str| None;
311 assert!(public_ip4("aws", "host.dns", &mut envs).is_none());
312 assert_eq!(
313 public_ip4("aws", "10.0.0.1", &mut envs).as_deref(),
314 Some("10.0.0.1"),
315 );
316 }
317}