1use crate::config::AwsConfig;
2use anyhow::Result;
3
4fn fmt_bytes(bytes: i64) -> String {
5 const GIB: i64 = 1 << 30;
6 const MIB: i64 = 1 << 20;
7 const KIB: i64 = 1 << 10;
8 if bytes >= GIB {
9 format!("{:.1} GiB", bytes as f64 / GIB as f64)
10 } else if bytes >= MIB {
11 format!("{:.1} MiB", bytes as f64 / MIB as f64)
12 } else if bytes >= KIB {
13 format!("{:.1} KiB", bytes as f64 / KIB as f64)
14 } else {
15 format!("{} B", bytes)
16 }
17}
18
19#[derive(Clone, Debug)]
20pub struct EfsFileSystem {
21 pub file_system_id: String,
22 pub file_system_arn: String,
23 pub name: String,
24 pub creation_token: String,
25 pub encrypted: String,
26 pub kms_key_id: String,
27 pub total_size: String,
28 pub size_in_standard: String,
29 pub size_in_ia: String,
30 pub size_in_archive: String,
31 pub provisioned_throughput: String,
32 pub throughput_mode: String,
33 pub life_cycle_state: String,
34 pub number_of_mount_targets: String,
35 pub owner_id: String,
36 pub creation_time: String,
37 pub performance_mode: String,
38 pub availability_zone: String,
39 pub replication_overwrite_protection: String,
40 pub total_size_bytes: i64,
42 pub size_in_standard_bytes: i64,
43 pub size_in_ia_bytes: i64,
44 pub size_in_archive_bytes: i64,
45 pub dns_name: String,
47 pub tags: Vec<(String, String)>,
49}
50
51pub struct EfsClient {
52 config: AwsConfig,
53}
54
55impl EfsClient {
56 pub fn new(config: AwsConfig) -> Self {
57 Self { config }
58 }
59
60 pub async fn list_file_systems(&self) -> Result<Vec<EfsFileSystem>> {
61 let client = self.config.efs_client();
62
63 let mut file_systems: Vec<EfsFileSystem> = Vec::new();
64 let mut marker: Option<String> = None;
65
66 loop {
67 let mut request = client.describe_file_systems();
68 if let Some(m) = marker {
69 request = request.marker(m);
70 }
71
72 let response = request.send().await?;
73
74 for fs in response.file_systems.unwrap_or_default() {
75 let name = fs.name().map(|s| s.to_string()).unwrap_or_default();
76
77 let sizes = fs.size_in_bytes();
78 let total_size = sizes.map(|s| fmt_bytes(s.value)).unwrap_or_default();
79 let size_in_standard = sizes
80 .and_then(|s| s.value_in_standard)
81 .map(fmt_bytes)
82 .unwrap_or_default();
83 let size_in_ia = sizes
84 .and_then(|s| s.value_in_ia)
85 .map(fmt_bytes)
86 .unwrap_or_default();
87 let size_in_archive = sizes
88 .and_then(|s| s.value_in_archive)
89 .map(fmt_bytes)
90 .unwrap_or_default();
91
92 let provisioned_throughput = fs
93 .provisioned_throughput_in_mibps()
94 .map(|v| format!("{:.1}", v))
95 .unwrap_or_default();
96
97 let throughput_mode = fs
98 .throughput_mode()
99 .map(|m| format!("{:?}", m))
100 .unwrap_or_default();
101
102 let life_cycle_state = format!("{:?}", fs.life_cycle_state());
103
104 let performance_mode = format!("{:?}", fs.performance_mode());
105
106 let availability_zone = fs.availability_zone_name().unwrap_or("").to_string();
107
108 let encrypted = match fs.encrypted() {
109 Some(true) => "Yes".to_string(),
110 Some(false) => "No".to_string(),
111 None => String::new(),
112 };
113
114 let kms_key_id = fs.kms_key_id().unwrap_or("").to_string();
115
116 let replication_overwrite_protection = fs
117 .file_system_protection()
118 .and_then(|p| p.replication_overwrite_protection())
119 .map(|r| format!("{:?}", r))
120 .unwrap_or_default();
121
122 let creation_time = fs
123 .creation_time()
124 .fmt(aws_smithy_types::date_time::Format::DateTime)
125 .unwrap_or_default();
126
127 file_systems.push(EfsFileSystem {
128 file_system_id: fs.file_system_id.clone(),
129 file_system_arn: fs.file_system_arn().unwrap_or("").to_string(),
130 name,
131 creation_token: fs.creation_token.clone(),
132 encrypted,
133 kms_key_id,
134 total_size,
135 size_in_standard,
136 size_in_ia,
137 size_in_archive,
138 provisioned_throughput,
139 throughput_mode,
140 life_cycle_state,
141 number_of_mount_targets: fs.number_of_mount_targets.to_string(),
142 owner_id: fs.owner_id.clone(),
143 creation_time,
144 performance_mode,
145 availability_zone,
146 replication_overwrite_protection,
147 total_size_bytes: sizes.map(|s| s.value).unwrap_or(0),
148 size_in_standard_bytes: sizes.and_then(|s| s.value_in_standard).unwrap_or(0),
149 size_in_ia_bytes: sizes.and_then(|s| s.value_in_ia).unwrap_or(0),
150 size_in_archive_bytes: sizes.and_then(|s| s.value_in_archive).unwrap_or(0),
151 dns_name: format!(
152 "{}.efs.{}.amazonaws.com",
153 fs.file_system_id.as_str(),
154 fs.file_system_arn()
156 .and_then(|a| a.split(':').nth(3))
157 .unwrap_or("unknown")
158 ),
159 tags: fs
160 .tags
161 .iter()
162 .map(|t| (t.key.clone(), t.value.clone()))
163 .collect(),
164 });
165 }
166
167 marker = response.next_marker;
168 if marker.is_none() {
169 break;
170 }
171 }
172
173 file_systems.sort_by(|a, b| b.creation_time.cmp(&a.creation_time));
175
176 Ok(file_systems)
177 }
178
179 pub async fn describe_access_points(
181 &self,
182 file_system_id: &str,
183 ) -> Result<Vec<EfsAccessPoint>> {
184 let client = self.config.efs_client();
185 let mut out: Vec<EfsAccessPoint> = Vec::new();
186 let mut next_token: Option<String> = None;
187
188 loop {
189 let mut req = client
190 .describe_access_points()
191 .file_system_id(file_system_id);
192 if let Some(t) = next_token {
193 req = req.next_token(t);
194 }
195 let resp = req.send().await?;
196
197 for ap in resp.access_points() {
198 let posix_user = ap
199 .posix_user()
200 .map(|p| format!("{}:{}", p.uid(), p.gid()))
201 .unwrap_or_default();
202
203 let (path, creation_info) = match ap.root_directory() {
204 Some(rd) => {
205 let path = rd.path().unwrap_or("/").to_string();
206 let ci = rd
207 .creation_info()
208 .map(|c| {
209 format!("{}:{} {}", c.owner_uid(), c.owner_gid(), c.permissions())
210 })
211 .unwrap_or_default();
212 (path, ci)
213 }
214 None => (String::new(), String::new()),
215 };
216
217 out.push(EfsAccessPoint {
218 access_point_id: ap.access_point_id().unwrap_or("").to_string(),
219 name: ap.name().unwrap_or("").to_string(),
220 path,
221 posix_user,
222 creation_info,
223 life_cycle_state: ap
224 .life_cycle_state()
225 .map(|s| format!("{:?}", s))
226 .unwrap_or_default(),
227 });
228 }
229
230 next_token = resp.next_token().map(|s| s.to_string());
231 if next_token.is_none() {
232 break;
233 }
234 }
235 Ok(out)
236 }
237
238 pub async fn describe_mount_targets(
241 &self,
242 file_system_id: &str,
243 ) -> Result<Vec<EfsMountTarget>> {
244 let client = self.config.efs_client();
245 let mut out: Vec<EfsMountTarget> = Vec::new();
246 let mut marker: Option<String> = None;
247
248 loop {
249 let mut req = client
250 .describe_mount_targets()
251 .file_system_id(file_system_id);
252 if let Some(m) = marker {
253 req = req.marker(m);
254 }
255 let resp = req.send().await?;
256
257 for mt in resp.mount_targets() {
258 let mount_target_id = mt.mount_target_id().to_string();
259
260 let security_groups = match client
263 .describe_mount_target_security_groups()
264 .mount_target_id(&mount_target_id)
265 .send()
266 .await
267 {
268 Ok(sg_resp) => sg_resp.security_groups().join(", "),
269 Err(_) => String::new(),
270 };
271
272 out.push(EfsMountTarget {
273 mount_target_id,
274 availability_zone_name: mt.availability_zone_name().unwrap_or("").to_string(),
275 availability_zone_id: mt.availability_zone_id().unwrap_or("").to_string(),
276 owner_id: mt.owner_id().unwrap_or("").to_string(),
277 file_system_id: mt.file_system_id().to_string(),
278 subnet_id: mt.subnet_id().to_string(),
279 vpc_id: mt.vpc_id().unwrap_or("").to_string(),
280 life_cycle_state: format!("{:?}", mt.life_cycle_state()),
281 ip_address: mt.ip_address().unwrap_or("").to_string(),
282 ipv6_address: mt.ipv6_address().unwrap_or("").to_string(),
283 network_interface_id: mt.network_interface_id().unwrap_or("").to_string(),
284 security_groups,
285 });
286 }
287
288 marker = resp.next_marker().map(|s| s.to_string());
289 if marker.is_none() {
290 break;
291 }
292 }
293
294 out.sort_by(|a, b| a.availability_zone_name.cmp(&b.availability_zone_name));
295 Ok(out)
296 }
297
298 pub async fn get_replication(&self, file_system_id: &str) -> Result<Option<String>> {
302 let client = self.config.efs_client();
303 let resp = client
304 .describe_replication_configurations()
305 .file_system_id(file_system_id)
306 .send()
307 .await;
308
309 match resp {
310 Ok(out) => {
311 let configs = out.replications();
312 if configs.is_empty() {
313 return Ok(None);
314 }
315 Ok(Some(format_replication(configs)))
316 }
317 Err(err) => {
318 if err
319 .as_service_error()
320 .map(|e| e.is_replication_not_found())
321 .unwrap_or(false)
322 {
323 Ok(None)
324 } else {
325 Err(err.into())
326 }
327 }
328 }
329 }
330
331 pub async fn get_file_system_policy(&self, file_system_id: &str) -> Result<Option<String>> {
334 let client = self.config.efs_client();
335 let resp = client
336 .describe_file_system_policy()
337 .file_system_id(file_system_id)
338 .send()
339 .await;
340
341 match resp {
342 Ok(out) => {
343 let Some(policy) = out.policy() else {
344 return Ok(None);
345 };
346 let pretty = serde_json::from_str::<serde_json::Value>(policy)
348 .ok()
349 .and_then(|v| serde_json::to_string_pretty(&v).ok())
350 .unwrap_or_else(|| policy.to_string());
351 Ok(Some(pretty))
352 }
353 Err(err) => {
354 if err
355 .as_service_error()
356 .map(|e| e.is_policy_not_found())
357 .unwrap_or(false)
358 {
359 Ok(None)
360 } else {
361 Err(err.into())
362 }
363 }
364 }
365 }
366
367 pub async fn get_monitoring_metrics(&self, file_system_id: &str) -> Result<EfsMonitoringData> {
370 let now = chrono::Utc::now();
371 let short_start = now - chrono::Duration::hours(3);
372 let long_start = now - chrono::Duration::hours(24);
374
375 let mut data = EfsMonitoringData::default();
376
377 {
380 let queries = vec![
381 efs_expr_query("e1", "(m1/1048576)/PERIOD(m1)", 60, false),
382 efs_expr_query("e2", "m2/1048576", 60, false),
383 efs_expr_query("e4", "((e1)*100)/(e2)", 60, true),
384 efs_metric_query(
385 "m1",
386 "MeteredIOBytes",
387 "Sum",
388 60,
389 file_system_id,
390 None,
391 false,
392 ),
393 efs_metric_query(
394 "m2",
395 "PermittedThroughput",
396 "Average",
397 60,
398 file_system_id,
399 None,
400 false,
401 ),
402 ];
403 let mut res = self.run_get_metric_data(queries, short_start, now).await?;
404 data.throughput_utilization = res.remove("e4").unwrap_or_default();
405 }
406
407 {
409 let queries = iops_or_throughput_queries("SampleCount", file_system_id);
410 let mut res = self.run_get_metric_data(queries, short_start, now).await?;
411 data.iops_data_write = res.remove("e2").unwrap_or_default();
412 data.iops_data_read = res.remove("e3").unwrap_or_default();
413 data.iops_metadata = res.remove("e4").unwrap_or_default();
414 }
415
416 {
418 let queries = iops_or_throughput_queries("Sum", file_system_id);
419 let mut res = self.run_get_metric_data(queries, short_start, now).await?;
420 data.throughput_data_write = res.remove("e2").unwrap_or_default();
421 data.throughput_data_read = res.remove("e3").unwrap_or_default();
422 data.throughput_metadata = res.remove("e4").unwrap_or_default();
423 }
424
425 {
427 let queries = vec![efs_metric_query(
428 "m1",
429 "PercentIOLimit",
430 "Average",
431 60,
432 file_system_id,
433 None,
434 true,
435 )];
436 let mut res = self.run_get_metric_data(queries, short_start, now).await?;
437 data.iops_utilization = res.remove("m1").unwrap_or_default();
438 }
439
440 {
442 let queries = vec![efs_metric_query(
443 "m1",
444 "ClientConnections",
445 "Sum",
446 60,
447 file_system_id,
448 None,
449 true,
450 )];
451 let mut res = self.run_get_metric_data(queries, short_start, now).await?;
452 data.client_connections = res.remove("m1").unwrap_or_default();
453 }
454
455 {
457 let queries = vec![
458 efs_metric_query(
459 "mStandard",
460 "StorageBytes",
461 "Average",
462 3600,
463 file_system_id,
464 Some(("StorageClass", "Standard")),
465 true,
466 ),
467 efs_metric_query(
468 "mIA",
469 "StorageBytes",
470 "Average",
471 3600,
472 file_system_id,
473 Some(("StorageClass", "IA")),
474 true,
475 ),
476 efs_metric_query(
477 "mArchive",
478 "StorageBytes",
479 "Average",
480 3600,
481 file_system_id,
482 Some(("StorageClass", "Archive")),
483 true,
484 ),
485 ];
486 let mut res = self.run_get_metric_data(queries, long_start, now).await?;
487 data.storage_standard = res.remove("mStandard").unwrap_or_default();
488 data.storage_ia = res.remove("mIA").unwrap_or_default();
489 data.storage_archive = res.remove("mArchive").unwrap_or_default();
490 }
491
492 Ok(data)
493 }
494
495 async fn run_get_metric_data(
497 &self,
498 queries: Vec<aws_sdk_cloudwatch::types::MetricDataQuery>,
499 start_time: chrono::DateTime<chrono::Utc>,
500 end_time: chrono::DateTime<chrono::Utc>,
501 ) -> Result<std::collections::HashMap<String, Vec<(i64, f64)>>> {
502 let client = self.config.cloudwatch_client();
503
504 let mut req = client
505 .get_metric_data()
506 .start_time(aws_sdk_cloudwatch::primitives::DateTime::from_millis(
507 start_time.timestamp_millis(),
508 ))
509 .end_time(aws_sdk_cloudwatch::primitives::DateTime::from_millis(
510 end_time.timestamp_millis(),
511 ));
512 for q in queries {
513 req = req.metric_data_queries(q);
514 }
515
516 let resp = req.send().await?;
517
518 let mut out: std::collections::HashMap<String, Vec<(i64, f64)>> =
519 std::collections::HashMap::new();
520 for r in resp.metric_data_results() {
521 let Some(id) = r.id() else { continue };
522 let mut series: Vec<(i64, f64)> = r
523 .timestamps()
524 .iter()
525 .zip(r.values().iter())
526 .map(|(ts, val)| (ts.as_secs_f64() as i64, *val))
527 .collect();
528 series.sort_by_key(|(ts, _)| *ts);
529 out.insert(id.to_string(), series);
530 }
531 Ok(out)
532 }
533}
534
535#[derive(Clone, Debug, Default)]
537pub struct EfsAccessPoint {
538 pub access_point_id: String,
539 pub name: String,
540 pub path: String,
541 pub posix_user: String,
543 pub creation_info: String,
545 pub life_cycle_state: String,
546}
547
548#[derive(Clone, Debug, Default)]
550pub struct EfsMountTarget {
551 pub mount_target_id: String,
552 pub availability_zone_name: String,
553 pub availability_zone_id: String,
554 pub owner_id: String,
555 pub file_system_id: String,
556 pub subnet_id: String,
557 pub vpc_id: String,
558 pub life_cycle_state: String,
559 pub ip_address: String,
560 pub ipv6_address: String,
561 pub network_interface_id: String,
562 pub security_groups: String,
564}
565
566fn format_replication(
568 configs: &[aws_sdk_efs::types::ReplicationConfigurationDescription],
569) -> String {
570 let mut lines: Vec<String> = Vec::new();
571 for cfg in configs {
572 lines.push(format!(
573 "Source file system: {}",
574 cfg.source_file_system_id()
575 ));
576 lines.push(format!(
577 "Source region: {}",
578 cfg.source_file_system_region()
579 ));
580 if let Ok(ts) = cfg
581 .creation_time()
582 .fmt(aws_smithy_types::date_time::Format::DateTime)
583 {
584 lines.push(format!("Created: {}", ts));
585 }
586 lines.push(String::new());
587 lines.push("Destinations:".to_string());
588 for d in cfg.destinations() {
589 lines.push(format!(
590 " • {} ({}) — {:?}",
591 d.file_system_id(),
592 d.region(),
593 d.status()
594 ));
595 if let Some(ts) = d.last_replicated_timestamp() {
596 if let Ok(s) = ts.fmt(aws_smithy_types::date_time::Format::DateTime) {
597 lines.push(format!(" last replicated: {}", s));
598 }
599 }
600 }
601 }
602 lines.join("\n")
603}
604
605#[derive(Default, Clone, Debug)]
607pub struct EfsMonitoringData {
608 pub throughput_utilization: Vec<(i64, f64)>,
610 pub iops_data_write: Vec<(i64, f64)>,
612 pub iops_data_read: Vec<(i64, f64)>,
613 pub iops_metadata: Vec<(i64, f64)>,
614 pub throughput_data_write: Vec<(i64, f64)>,
616 pub throughput_data_read: Vec<(i64, f64)>,
617 pub throughput_metadata: Vec<(i64, f64)>,
618 pub iops_utilization: Vec<(i64, f64)>,
620 pub client_connections: Vec<(i64, f64)>,
622 pub storage_standard: Vec<(i64, f64)>,
624 pub storage_ia: Vec<(i64, f64)>,
625 pub storage_archive: Vec<(i64, f64)>,
626}
627
628fn efs_metric_query(
630 id: &str,
631 metric_name: &str,
632 stat: &str,
633 period: i32,
634 file_system_id: &str,
635 extra_dimension: Option<(&str, &str)>,
636 return_data: bool,
637) -> aws_sdk_cloudwatch::types::MetricDataQuery {
638 use aws_sdk_cloudwatch::types::{Dimension, Metric, MetricDataQuery, MetricStat};
639
640 let mut metric_b = Metric::builder()
641 .namespace("AWS/EFS")
642 .metric_name(metric_name);
643 if let Some((k, v)) = extra_dimension {
645 metric_b = metric_b.dimensions(Dimension::builder().name(k).value(v).build());
646 }
647 metric_b = metric_b.dimensions(
648 Dimension::builder()
649 .name("FileSystemId")
650 .value(file_system_id)
651 .build(),
652 );
653
654 let stat = MetricStat::builder()
655 .metric(metric_b.build())
656 .period(period)
657 .stat(stat)
658 .build();
659
660 MetricDataQuery::builder()
661 .id(id)
662 .metric_stat(stat)
663 .return_data(return_data)
664 .build()
665}
666
667fn efs_expr_query(
669 id: &str,
670 expression: &str,
671 period: i32,
672 return_data: bool,
673) -> aws_sdk_cloudwatch::types::MetricDataQuery {
674 aws_sdk_cloudwatch::types::MetricDataQuery::builder()
675 .id(id)
676 .expression(expression)
677 .period(period)
678 .return_data(return_data)
679 .build()
680}
681
682fn iops_or_throughput_queries(
686 stat: &str,
687 file_system_id: &str,
688) -> Vec<aws_sdk_cloudwatch::types::MetricDataQuery> {
689 vec![
690 efs_expr_query("e2", "(m2*100)/m1", 60, true),
691 efs_expr_query("e3", "(m3*100)/m1", 60, true),
692 efs_expr_query("e4", "(m4*100)/m1", 60, true),
693 efs_metric_query("m1", "TotalIOBytes", stat, 60, file_system_id, None, false),
694 efs_metric_query(
695 "m2",
696 "DataWriteIOBytes",
697 stat,
698 60,
699 file_system_id,
700 None,
701 false,
702 ),
703 efs_metric_query(
704 "m3",
705 "DataReadIOBytes",
706 stat,
707 60,
708 file_system_id,
709 None,
710 false,
711 ),
712 efs_metric_query(
713 "m4",
714 "MetadataIOBytes",
715 stat,
716 60,
717 file_system_id,
718 None,
719 false,
720 ),
721 ]
722}