Skip to main content

rusticity_core/
ec2.rs

1use crate::config::AwsConfig;
2use anyhow::Result;
3
4#[derive(Clone, Debug)]
5pub struct Instance {
6    pub instance_id: String,
7    pub name: String,
8    pub state: String,
9    pub instance_type: String,
10    pub availability_zone: String,
11    pub public_ipv4_dns: String,
12    pub public_ipv4_address: String,
13    pub elastic_ip: String,
14    pub ipv6_ips: String,
15    pub monitoring: String,
16    pub security_groups: String,
17    pub key_name: String,
18    pub launch_time: String,
19    pub platform_details: String,
20    pub status_checks: String,
21    pub alarm_status: String,
22}
23
24#[derive(Clone, Debug)]
25pub struct InstanceTag {
26    pub key: String,
27    pub value: String,
28}
29
30pub struct Ec2Client {
31    config: AwsConfig,
32}
33
34impl Ec2Client {
35    pub fn new(config: AwsConfig) -> Self {
36        Self { config }
37    }
38
39    pub async fn list_instances(&self) -> Result<Vec<Instance>> {
40        let client = self.config.ec2_client().await;
41        let mut instances = Vec::new();
42        let mut next_token: Option<String> = None;
43
44        loop {
45            let mut request = client.describe_instances();
46            if let Some(token) = next_token {
47                request = request.next_token(token);
48            }
49
50            let response = request.send().await?;
51
52            if let Some(reservations) = response.reservations {
53                for reservation in reservations {
54                    if let Some(insts) = reservation.instances {
55                        for inst in insts {
56                            let tags: std::collections::HashMap<String, String> = inst
57                                .tags()
58                                .iter()
59                                .filter_map(|t| {
60                                    Some((t.key()?.to_string(), t.value()?.to_string()))
61                                })
62                                .collect();
63
64                            let name = tags.get("Name").cloned().unwrap_or_default();
65
66                            let state = inst
67                                .state()
68                                .and_then(|s| s.name())
69                                .map(|n| n.as_str().to_string())
70                                .unwrap_or_default();
71
72                            let security_groups = inst
73                                .security_groups()
74                                .iter()
75                                .filter_map(|sg| sg.group_name())
76                                .collect::<Vec<_>>()
77                                .join(", ");
78
79                            let ipv6_ips = inst
80                                .network_interfaces()
81                                .iter()
82                                .flat_map(|ni| ni.ipv6_addresses())
83                                .filter_map(|ip| ip.ipv6_address())
84                                .collect::<Vec<_>>()
85                                .join(", ");
86
87                            let launch_time = inst
88                                .launch_time()
89                                .map(|dt| {
90                                    let timestamp = dt.secs();
91                                    chrono::DateTime::from_timestamp(timestamp, 0)
92                                        .map(|dt| dt.format("%Y-%m-%d %H:%M:%S (UTC)").to_string())
93                                        .unwrap_or_default()
94                                })
95                                .unwrap_or_default();
96
97                            instances.push(Instance {
98                                instance_id: inst.instance_id().unwrap_or("").to_string(),
99                                name,
100                                state,
101                                instance_type: inst
102                                    .instance_type()
103                                    .map(|t| t.as_str().to_string())
104                                    .unwrap_or_default(),
105                                availability_zone: inst
106                                    .placement()
107                                    .and_then(|p| p.availability_zone())
108                                    .unwrap_or("")
109                                    .to_string(),
110                                public_ipv4_dns: inst.public_dns_name().unwrap_or("").to_string(),
111                                public_ipv4_address: inst
112                                    .public_ip_address()
113                                    .unwrap_or("")
114                                    .to_string(),
115                                elastic_ip: String::new(),
116                                ipv6_ips,
117                                monitoring: inst
118                                    .monitoring()
119                                    .and_then(|m| m.state())
120                                    .map(|s| s.as_str().to_string())
121                                    .unwrap_or_default(),
122                                security_groups,
123                                key_name: inst.key_name().unwrap_or("").to_string(),
124                                launch_time,
125                                platform_details: inst.platform_details().unwrap_or("").to_string(),
126                                status_checks: String::new(),
127                                alarm_status: String::new(),
128                            });
129                        }
130                    }
131                }
132            }
133
134            next_token = response.next_token;
135            if next_token.is_none() {
136                break;
137            }
138        }
139
140        Ok(instances)
141    }
142
143    pub async fn list_tags(&self, instance_id: &str) -> Result<Vec<InstanceTag>> {
144        let client = self.config.ec2_client().await;
145
146        let response = client
147            .describe_tags()
148            .filters(
149                aws_sdk_ec2::types::Filter::builder()
150                    .name("resource-id")
151                    .values(instance_id)
152                    .build(),
153            )
154            .send()
155            .await?;
156
157        let mut tags = Vec::new();
158        if let Some(tag_list) = response.tags {
159            for tag in tag_list {
160                if let (Some(key), Some(value)) = (tag.key, tag.value) {
161                    tags.push(InstanceTag { key, value });
162                }
163            }
164        }
165
166        Ok(tags)
167    }
168
169    pub async fn get_cpu_metrics(&self, instance_id: &str) -> Result<Vec<(i64, f64)>> {
170        let client = self.config.cloudwatch_client().await;
171        let now = chrono::Utc::now();
172        let start_time = now - chrono::Duration::hours(3);
173
174        let response = client
175            .get_metric_statistics()
176            .namespace("AWS/EC2")
177            .metric_name("CPUUtilization")
178            .dimensions(
179                aws_sdk_cloudwatch::types::Dimension::builder()
180                    .name("InstanceId")
181                    .value(instance_id)
182                    .build(),
183            )
184            .start_time(aws_sdk_cloudwatch::primitives::DateTime::from_millis(
185                start_time.timestamp_millis(),
186            ))
187            .end_time(aws_sdk_cloudwatch::primitives::DateTime::from_millis(
188                now.timestamp_millis(),
189            ))
190            .period(300)
191            .statistics(aws_sdk_cloudwatch::types::Statistic::Average)
192            .send()
193            .await?;
194
195        let mut data_points = Vec::new();
196        if let Some(datapoints) = response.datapoints {
197            for dp in datapoints {
198                if let (Some(timestamp), Some(value)) = (dp.timestamp, dp.average) {
199                    data_points.push((timestamp.secs(), value));
200                }
201            }
202        }
203
204        data_points.sort_by_key(|(ts, _)| *ts);
205        Ok(data_points)
206    }
207
208    pub async fn get_network_in_metrics(&self, instance_id: &str) -> Result<Vec<(i64, f64)>> {
209        let client = self.config.cloudwatch_client().await;
210        let now = chrono::Utc::now();
211        let start_time = now - chrono::Duration::hours(3);
212
213        let response = client
214            .get_metric_statistics()
215            .namespace("AWS/EC2")
216            .metric_name("NetworkIn")
217            .dimensions(
218                aws_sdk_cloudwatch::types::Dimension::builder()
219                    .name("InstanceId")
220                    .value(instance_id)
221                    .build(),
222            )
223            .start_time(aws_sdk_cloudwatch::primitives::DateTime::from_millis(
224                start_time.timestamp_millis(),
225            ))
226            .end_time(aws_sdk_cloudwatch::primitives::DateTime::from_millis(
227                now.timestamp_millis(),
228            ))
229            .period(300)
230            .statistics(aws_sdk_cloudwatch::types::Statistic::Average)
231            .send()
232            .await?;
233
234        let mut data_points = Vec::new();
235        if let Some(datapoints) = response.datapoints {
236            for dp in datapoints {
237                if let (Some(timestamp), Some(value)) = (dp.timestamp, dp.average) {
238                    data_points.push((timestamp.secs(), value));
239                }
240            }
241        }
242
243        data_points.sort_by_key(|(ts, _)| *ts);
244        Ok(data_points)
245    }
246
247    pub async fn get_network_out_metrics(&self, instance_id: &str) -> Result<Vec<(i64, f64)>> {
248        let client = self.config.cloudwatch_client().await;
249        let now = chrono::Utc::now();
250        let start_time = now - chrono::Duration::hours(3);
251
252        let response = client
253            .get_metric_statistics()
254            .namespace("AWS/EC2")
255            .metric_name("NetworkOut")
256            .dimensions(
257                aws_sdk_cloudwatch::types::Dimension::builder()
258                    .name("InstanceId")
259                    .value(instance_id)
260                    .build(),
261            )
262            .start_time(aws_sdk_cloudwatch::primitives::DateTime::from_millis(
263                start_time.timestamp_millis(),
264            ))
265            .end_time(aws_sdk_cloudwatch::primitives::DateTime::from_millis(
266                now.timestamp_millis(),
267            ))
268            .period(300)
269            .statistics(aws_sdk_cloudwatch::types::Statistic::Average)
270            .send()
271            .await?;
272
273        let mut data_points = Vec::new();
274        if let Some(datapoints) = response.datapoints {
275            for dp in datapoints {
276                if let (Some(timestamp), Some(value)) = (dp.timestamp, dp.average) {
277                    data_points.push((timestamp.secs(), value));
278                }
279            }
280        }
281
282        data_points.sort_by_key(|(ts, _)| *ts);
283        Ok(data_points)
284    }
285
286    pub async fn get_network_packets_in_metrics(
287        &self,
288        instance_id: &str,
289    ) -> Result<Vec<(i64, f64)>> {
290        let client = self.config.cloudwatch_client().await;
291        let now = chrono::Utc::now();
292        let start_time = now - chrono::Duration::hours(3);
293
294        let response = client
295            .get_metric_statistics()
296            .namespace("AWS/EC2")
297            .metric_name("NetworkPacketsIn")
298            .dimensions(
299                aws_sdk_cloudwatch::types::Dimension::builder()
300                    .name("InstanceId")
301                    .value(instance_id)
302                    .build(),
303            )
304            .start_time(aws_sdk_cloudwatch::primitives::DateTime::from_millis(
305                start_time.timestamp_millis(),
306            ))
307            .end_time(aws_sdk_cloudwatch::primitives::DateTime::from_millis(
308                now.timestamp_millis(),
309            ))
310            .period(300)
311            .statistics(aws_sdk_cloudwatch::types::Statistic::Average)
312            .send()
313            .await?;
314
315        let mut data_points = Vec::new();
316        if let Some(datapoints) = response.datapoints {
317            for dp in datapoints {
318                if let (Some(timestamp), Some(value)) = (dp.timestamp, dp.average) {
319                    data_points.push((timestamp.secs(), value));
320                }
321            }
322        }
323
324        data_points.sort_by_key(|(ts, _)| *ts);
325        Ok(data_points)
326    }
327
328    pub async fn get_network_packets_out_metrics(
329        &self,
330        instance_id: &str,
331    ) -> Result<Vec<(i64, f64)>> {
332        let client = self.config.cloudwatch_client().await;
333        let now = chrono::Utc::now();
334        let start_time = now - chrono::Duration::hours(3);
335
336        let response = client
337            .get_metric_statistics()
338            .namespace("AWS/EC2")
339            .metric_name("NetworkPacketsOut")
340            .dimensions(
341                aws_sdk_cloudwatch::types::Dimension::builder()
342                    .name("InstanceId")
343                    .value(instance_id)
344                    .build(),
345            )
346            .start_time(aws_sdk_cloudwatch::primitives::DateTime::from_millis(
347                start_time.timestamp_millis(),
348            ))
349            .end_time(aws_sdk_cloudwatch::primitives::DateTime::from_millis(
350                now.timestamp_millis(),
351            ))
352            .period(300)
353            .statistics(aws_sdk_cloudwatch::types::Statistic::Average)
354            .send()
355            .await?;
356
357        let mut data_points = Vec::new();
358        if let Some(datapoints) = response.datapoints {
359            for dp in datapoints {
360                if let (Some(timestamp), Some(value)) = (dp.timestamp, dp.average) {
361                    data_points.push((timestamp.secs(), value));
362                }
363            }
364        }
365
366        data_points.sort_by_key(|(ts, _)| *ts);
367        Ok(data_points)
368    }
369
370    pub async fn get_metadata_no_token_metrics(
371        &self,
372        instance_id: &str,
373    ) -> Result<Vec<(i64, f64)>> {
374        let client = self.config.cloudwatch_client().await;
375        let now = chrono::Utc::now();
376        let start_time = now - chrono::Duration::hours(3);
377
378        let response = client
379            .get_metric_statistics()
380            .namespace("AWS/EC2")
381            .metric_name("MetadataNoToken")
382            .dimensions(
383                aws_sdk_cloudwatch::types::Dimension::builder()
384                    .name("InstanceId")
385                    .value(instance_id)
386                    .build(),
387            )
388            .start_time(aws_sdk_cloudwatch::primitives::DateTime::from_millis(
389                start_time.timestamp_millis(),
390            ))
391            .end_time(aws_sdk_cloudwatch::primitives::DateTime::from_millis(
392                now.timestamp_millis(),
393            ))
394            .period(300)
395            .statistics(aws_sdk_cloudwatch::types::Statistic::Average)
396            .send()
397            .await?;
398
399        let mut data_points = Vec::new();
400        if let Some(datapoints) = response.datapoints {
401            for dp in datapoints {
402                if let (Some(timestamp), Some(value)) = (dp.timestamp, dp.average) {
403                    data_points.push((timestamp.secs(), value));
404                }
405            }
406        }
407
408        data_points.sort_by_key(|(ts, _)| *ts);
409        Ok(data_points)
410    }
411}