Skip to main content

rusticity_core/
fsx.rs

1use crate::config::AwsConfig;
2use anyhow::Result;
3
4/// An FSx file system, flattened for display.
5#[derive(Clone, Debug, Default)]
6pub struct FsxFileSystem {
7    /// Name tag value.
8    pub name: String,
9    pub file_system_id: String,
10    pub file_system_arn: String,
11    /// WINDOWS / LUSTRE / ONTAP / OPENZFS.
12    pub file_system_type: String,
13    /// Lifecycle state (AVAILABLE, CREATING, ...).
14    pub status: String,
15    /// Deployment type (from the type-specific configuration).
16    pub deployment_type: String,
17    /// Storage class / type (SSD, HDD).
18    pub storage_class: String,
19    /// Storage capacity, e.g. "1200 GiB".
20    pub storage_capacity: String,
21    /// Raw storage capacity in GiB.
22    pub storage_capacity_gib: i64,
23    /// Throughput capacity, e.g. "512 MB/s" (or "-" when not applicable).
24    pub throughput_capacity: String,
25    /// Per-unit storage throughput (Lustre), MB/s/TiB. 0 when not applicable.
26    pub per_unit_throughput: i64,
27    /// Total throughput in MB/s (Lustre = per-unit × capacity). 0 when unknown.
28    pub total_throughput_mbps: i64,
29    /// Provisioned IOPS (or "-" when not applicable).
30    pub provisioned_iops: String,
31    pub creation_time: String,
32    /// Lustre file system version (or "-" for non-Lustre).
33    pub lustre_version: String,
34    /// Data compression type (Lustre): NONE / LZ4.
35    pub data_compression_type: String,
36    /// EFA enabled (Lustre): "Enabled" / "Disabled" / "".
37    pub efa: String,
38    /// Root squash (Lustre): "Enabled" / "Disabled" / "".
39    pub root_squash: String,
40    /// Lustre mount name.
41    pub mount_name: String,
42    /// Comma-separated subnet IDs (network placement).
43    pub subnet_ids: String,
44    pub vpc_id: String,
45    pub kms_key_id: String,
46    /// DNS name of the file system.
47    pub dns_name: String,
48    /// Comma-separated network interface IDs.
49    pub network_interface_ids: String,
50    /// Raw weekly maintenance start ("d:HH:MM", e.g. "4:07:00").
51    pub weekly_maintenance_start: String,
52    /// Raw daily automatic backup start ("HH:MM").
53    pub daily_automatic_backup_start: String,
54    /// Automatic backup retention in days (0 = disabled/unknown).
55    pub automatic_backup_retention_days: i32,
56    /// Tags as (key, value) pairs.
57    pub tags: Vec<(String, String)>,
58    /// Administrative actions / updates on the file system.
59    pub updates: Vec<FsxUpdate>,
60}
61
62/// An FSx administrative action (Updates tab).
63#[derive(Clone, Debug, Default)]
64pub struct FsxUpdate {
65    pub update_type: String,
66    pub target_value: String,
67    pub status: String,
68    pub progress_percent: String,
69    pub request_time: String,
70    /// Raw request time (RFC3339) used for sorting.
71    pub request_time_raw: String,
72}
73
74/// An FSx backup (Backups tab).
75#[derive(Clone, Debug, Default)]
76pub struct FsxBackup {
77    pub name: String,
78    pub backup_id: String,
79    pub file_system_type: String,
80    pub lifecycle_state: String,
81    pub progress_percent: String,
82    pub backup_type: String,
83    pub deployment_type: String,
84    pub storage_class: String,
85    pub storage: String,
86    pub resource_id: String,
87    pub resource_name: String,
88    pub backup_time: String,
89    pub active_directory: String,
90    pub lustre_version: String,
91    pub kms_key_id: String,
92}
93
94pub struct FsxClient {
95    config: AwsConfig,
96}
97
98impl FsxClient {
99    pub fn new(config: AwsConfig) -> Self {
100        Self { config }
101    }
102
103    pub async fn list_file_systems(&self) -> Result<Vec<FsxFileSystem>> {
104        let client = self.config.fsx_client();
105
106        let mut file_systems: Vec<FsxFileSystem> = Vec::new();
107        let mut next_token: Option<String> = None;
108
109        loop {
110            let mut request = client.describe_file_systems();
111            if let Some(t) = next_token {
112                request = request.next_token(t);
113            }
114            let response = request.send().await?;
115
116            for fs in response.file_systems() {
117                let file_system_type = fs
118                    .file_system_type()
119                    .map(|t| format!("{:?}", t))
120                    .unwrap_or_default();
121
122                let status = fs
123                    .lifecycle()
124                    .map(|s| format!("{:?}", s))
125                    .unwrap_or_default();
126
127                let storage_class = fs
128                    .storage_type()
129                    .map(|s| format!("{:?}", s))
130                    .unwrap_or_default();
131
132                let storage_capacity_gib = fs.storage_capacity().unwrap_or(0) as i64;
133                let storage_capacity = if storage_capacity_gib > 0 {
134                    format!("{} GiB", storage_capacity_gib)
135                } else {
136                    String::new()
137                };
138
139                // Deployment type, throughput, and provisioned IOPS live in the
140                // type-specific configuration block. Lustre also carries per-unit
141                // throughput, compression, EFA, root squash, and mount name.
142                let mut per_unit_throughput: i64 = 0;
143                let mut data_compression_type = String::new();
144                let mut efa = String::new();
145                let mut root_squash = String::new();
146                let mut mount_name = String::new();
147                let (deployment_type, throughput_capacity, provisioned_iops) =
148                    if let Some(w) = fs.windows_configuration() {
149                        (
150                            w.deployment_type().map(|d| format!("{:?}", d)),
151                            w.throughput_capacity().map(|t| format!("{} MB/s", t)),
152                            None,
153                        )
154                    } else if let Some(l) = fs.lustre_configuration() {
155                        per_unit_throughput = l.per_unit_storage_throughput().unwrap_or(0) as i64;
156                        data_compression_type = l
157                            .data_compression_type()
158                            .map(|d| format!("{:?}", d))
159                            .unwrap_or_default();
160                        efa = match l.efa_enabled() {
161                            Some(true) => "Enabled".to_string(),
162                            Some(false) => "Disabled".to_string(),
163                            None => String::new(),
164                        };
165                        root_squash =
166                            match l.root_squash_configuration().and_then(|r| r.root_squash()) {
167                                Some(rs) if !rs.is_empty() => "Enabled".to_string(),
168                                _ => "Disabled".to_string(),
169                            };
170                        mount_name = l.mount_name().unwrap_or("").to_string();
171                        (
172                            l.deployment_type().map(|d| format!("{:?}", d)),
173                            if per_unit_throughput > 0 {
174                                Some(format!("{} MB/s/TiB", per_unit_throughput))
175                            } else {
176                                None
177                            },
178                            None,
179                        )
180                    } else if let Some(o) = fs.ontap_configuration() {
181                        (
182                            o.deployment_type().map(|d| format!("{:?}", d)),
183                            o.throughput_capacity().map(|t| format!("{} MB/s", t)),
184                            o.disk_iops_configuration()
185                                .and_then(|d| d.iops())
186                                .map(|i| i.to_string()),
187                        )
188                    } else if let Some(z) = fs.open_zfs_configuration() {
189                        (
190                            z.deployment_type().map(|d| format!("{:?}", d)),
191                            z.throughput_capacity().map(|t| format!("{} MB/s", t)),
192                            z.disk_iops_configuration()
193                                .and_then(|d| d.iops())
194                                .map(|i| i.to_string()),
195                        )
196                    } else {
197                        (None, None, None)
198                    };
199
200                // Total throughput for Lustre = per-unit × capacity (in TB, /1000).
201                let total_throughput_mbps = if per_unit_throughput > 0 {
202                    per_unit_throughput * storage_capacity_gib / 1000
203                } else {
204                    0
205                };
206
207                // Maintenance / automatic-backup settings live in the type config.
208                let (weekly_maintenance_start, daily_automatic_backup_start, retention_days) =
209                    if let Some(w) = fs.windows_configuration() {
210                        (
211                            w.weekly_maintenance_start_time(),
212                            w.daily_automatic_backup_start_time(),
213                            w.automatic_backup_retention_days(),
214                        )
215                    } else if let Some(l) = fs.lustre_configuration() {
216                        (
217                            l.weekly_maintenance_start_time(),
218                            l.daily_automatic_backup_start_time(),
219                            l.automatic_backup_retention_days(),
220                        )
221                    } else if let Some(o) = fs.ontap_configuration() {
222                        (
223                            o.weekly_maintenance_start_time(),
224                            o.daily_automatic_backup_start_time(),
225                            o.automatic_backup_retention_days(),
226                        )
227                    } else if let Some(z) = fs.open_zfs_configuration() {
228                        (
229                            z.weekly_maintenance_start_time(),
230                            z.daily_automatic_backup_start_time(),
231                            z.automatic_backup_retention_days(),
232                        )
233                    } else {
234                        (None, None, None)
235                    };
236                let weekly_maintenance_start = weekly_maintenance_start.unwrap_or("").to_string();
237                let daily_automatic_backup_start =
238                    daily_automatic_backup_start.unwrap_or("").to_string();
239                let automatic_backup_retention_days = retention_days.unwrap_or(0);
240
241                let dns_name = fs.dns_name().unwrap_or("").to_string();
242                let network_interface_ids = fs.network_interface_ids().join(", ");
243
244                // Administrative actions (Updates tab), sorted by request time ASC.
245                let mut updates: Vec<FsxUpdate> = fs
246                    .administrative_actions()
247                    .iter()
248                    .map(|a| {
249                        let request_time_raw = a
250                            .request_time()
251                            .and_then(|t| t.fmt(aws_smithy_types::date_time::Format::DateTime).ok())
252                            .unwrap_or_default();
253                        let target_value = a
254                            .target_file_system_values()
255                            .map(|t| {
256                                if t.storage_capacity().unwrap_or(0) > 0 {
257                                    format!("{} GiB", t.storage_capacity().unwrap_or(0))
258                                } else if let Some(l) = t.lustre_configuration() {
259                                    l.per_unit_storage_throughput()
260                                        .map(|v| format!("{} MB/s/TiB", v))
261                                        .unwrap_or_default()
262                                } else if let Some(w) = t.windows_configuration() {
263                                    w.throughput_capacity()
264                                        .map(|v| format!("{} MB/s", v))
265                                        .unwrap_or_default()
266                                } else {
267                                    String::new()
268                                }
269                            })
270                            .unwrap_or_default();
271                        FsxUpdate {
272                            update_type: a
273                                .administrative_action_type()
274                                .map(|t| format!("{:?}", t))
275                                .unwrap_or_default(),
276                            target_value,
277                            status: a.status().map(|s| format!("{:?}", s)).unwrap_or_default(),
278                            progress_percent: a
279                                .progress_percent()
280                                .map(|p| format!("{}%", p))
281                                .unwrap_or_default(),
282                            request_time: request_time_raw.clone(),
283                            request_time_raw,
284                        }
285                    })
286                    .collect();
287                updates.sort_by(|a, b| a.request_time_raw.cmp(&b.request_time_raw));
288
289                let creation_time = fs
290                    .creation_time()
291                    .and_then(|t| t.fmt(aws_smithy_types::date_time::Format::DateTime).ok())
292                    .unwrap_or_default();
293
294                // Lustre version only applies to Lustre file systems.
295                let lustre_version =
296                    if fs.file_system_type() == Some(&aws_sdk_fsx::types::FileSystemType::Lustre) {
297                        fs.file_system_type_version().unwrap_or("").to_string()
298                    } else {
299                        String::new()
300                    };
301
302                let name = fs
303                    .tags()
304                    .iter()
305                    .find(|t| t.key() == Some("Name"))
306                    .and_then(|t| t.value())
307                    .unwrap_or("")
308                    .to_string();
309
310                let subnet_ids = fs.subnet_ids().join(", ");
311
312                let tags: Vec<(String, String)> = fs
313                    .tags()
314                    .iter()
315                    .filter_map(|t| Some((t.key()?.to_string(), t.value()?.to_string())))
316                    .collect();
317
318                file_systems.push(FsxFileSystem {
319                    name,
320                    file_system_id: fs.file_system_id().unwrap_or("").to_string(),
321                    file_system_arn: fs.resource_arn().unwrap_or("").to_string(),
322                    file_system_type,
323                    status,
324                    deployment_type: deployment_type.unwrap_or_default(),
325                    storage_class,
326                    storage_capacity,
327                    storage_capacity_gib,
328                    throughput_capacity: throughput_capacity.unwrap_or_default(),
329                    per_unit_throughput,
330                    total_throughput_mbps,
331                    provisioned_iops: provisioned_iops.unwrap_or_default(),
332                    creation_time,
333                    lustre_version,
334                    data_compression_type,
335                    efa,
336                    root_squash,
337                    mount_name,
338                    subnet_ids,
339                    vpc_id: fs.vpc_id().unwrap_or("").to_string(),
340                    kms_key_id: fs.kms_key_id().unwrap_or("").to_string(),
341                    dns_name,
342                    network_interface_ids,
343                    weekly_maintenance_start,
344                    daily_automatic_backup_start,
345                    automatic_backup_retention_days,
346                    tags,
347                    updates,
348                });
349            }
350
351            next_token = response.next_token().map(|s| s.to_string());
352            if next_token.is_none() {
353                break;
354            }
355        }
356
357        // Sort by creation_time descending.
358        file_systems.sort_by(|a, b| b.creation_time.cmp(&a.creation_time));
359
360        Ok(file_systems)
361    }
362
363    /// List backups for a specific file system (Backups tab).
364    pub async fn describe_backups(&self, file_system_id: &str) -> Result<Vec<FsxBackup>> {
365        use aws_sdk_fsx::types::Filter;
366        let client = self.config.fsx_client();
367
368        let mut out: Vec<FsxBackup> = Vec::new();
369        let mut next_token: Option<String> = None;
370
371        loop {
372            let mut req = client.describe_backups().filters(
373                Filter::builder()
374                    .name(aws_sdk_fsx::types::FilterName::FileSystemId)
375                    .values(file_system_id)
376                    .build(),
377            );
378            if let Some(t) = next_token {
379                req = req.next_token(t);
380            }
381            let resp = req.send().await?;
382
383            for b in resp.backups() {
384                let name = b
385                    .tags()
386                    .iter()
387                    .find(|t| t.key() == Some("Name"))
388                    .and_then(|t| t.value())
389                    .unwrap_or("")
390                    .to_string();
391
392                let fs = b.file_system();
393                let file_system_type = fs
394                    .and_then(|f| f.file_system_type())
395                    .map(|t| format!("{:?}", t))
396                    .unwrap_or_default();
397                let deployment_type = fs.map(deployment_type_of).unwrap_or_default();
398                let storage_class = fs
399                    .and_then(|f| f.storage_type())
400                    .map(|s| format!("{:?}", s))
401                    .unwrap_or_default();
402                let storage = fs
403                    .and_then(|f| f.storage_capacity())
404                    .map(|c| format!("{} GiB", c))
405                    .unwrap_or_default();
406                let resource_id = fs
407                    .and_then(|f| f.file_system_id())
408                    .unwrap_or("")
409                    .to_string();
410                let resource_name = fs
411                    .map(|f| {
412                        f.tags()
413                            .iter()
414                            .find(|t| t.key() == Some("Name"))
415                            .and_then(|t| t.value())
416                            .unwrap_or("")
417                            .to_string()
418                    })
419                    .unwrap_or_default();
420                let lustre_version = fs
421                    .filter(|f| {
422                        f.file_system_type() == Some(&aws_sdk_fsx::types::FileSystemType::Lustre)
423                    })
424                    .and_then(|f| f.file_system_type_version())
425                    .unwrap_or("")
426                    .to_string();
427                let active_directory = b
428                    .directory_information()
429                    .and_then(|d| d.active_directory_id())
430                    .unwrap_or("")
431                    .to_string();
432
433                out.push(FsxBackup {
434                    name,
435                    backup_id: b.backup_id().unwrap_or("").to_string(),
436                    file_system_type,
437                    lifecycle_state: b
438                        .lifecycle()
439                        .map(|s| format!("{:?}", s))
440                        .unwrap_or_default(),
441                    progress_percent: b
442                        .progress_percent()
443                        .map(|p| format!("{}%", p))
444                        .unwrap_or_default(),
445                    backup_type: b.r#type().map(|t| format!("{:?}", t)).unwrap_or_default(),
446                    deployment_type,
447                    storage_class,
448                    storage,
449                    resource_id,
450                    resource_name,
451                    backup_time: b
452                        .creation_time()
453                        .and_then(|t| t.fmt(aws_smithy_types::date_time::Format::DateTime).ok())
454                        .unwrap_or_default(),
455                    active_directory,
456                    lustre_version,
457                    kms_key_id: b.kms_key_id().unwrap_or("").to_string(),
458                });
459            }
460
461            next_token = resp.next_token().map(|s| s.to_string());
462            if next_token.is_none() {
463                break;
464            }
465        }
466
467        // Sort by backup time descending (newest first).
468        out.sort_by(|a, b| b.backup_time.cmp(&a.backup_time));
469        Ok(out)
470    }
471}
472
473/// Extract the deployment type from whichever type-specific config is present.
474fn deployment_type_of(fs: &aws_sdk_fsx::types::FileSystem) -> String {
475    if let Some(w) = fs.windows_configuration() {
476        w.deployment_type()
477            .map(|d| format!("{:?}", d))
478            .unwrap_or_default()
479    } else if let Some(l) = fs.lustre_configuration() {
480        l.deployment_type()
481            .map(|d| format!("{:?}", d))
482            .unwrap_or_default()
483    } else if let Some(o) = fs.ontap_configuration() {
484        o.deployment_type()
485            .map(|d| format!("{:?}", d))
486            .unwrap_or_default()
487    } else if let Some(z) = fs.open_zfs_configuration() {
488        z.deployment_type()
489            .map(|d| format!("{:?}", d))
490            .unwrap_or_default()
491    } else {
492        String::new()
493    }
494}