aws_manager/ec2/
metadata.rs

1use crate::errors::{Error, Result};
2use chrono::{DateTime, Utc};
3use reqwest::ClientBuilder;
4use serde::{Deserialize, Serialize};
5use tokio::time::Duration;
6
7/// Fetches the instance ID on the host EC2 machine.
8/// ref. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-categories.html
9pub async fn fetch_instance_id() -> Result<String> {
10    fetch_metadata_by_path("instance-id").await
11}
12
13/// Fetches the public hostname of the host EC2 machine.
14/// ref. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-categories.html
15pub async fn fetch_public_hostname() -> Result<String> {
16    fetch_metadata_by_path("public-hostname").await
17}
18
19/// Fetches the public IPv4 address of the host EC2 machine.
20/// ref. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-categories.html
21pub async fn fetch_public_ipv4() -> Result<String> {
22    fetch_metadata_by_path("public-ipv4").await
23}
24
25/// Fetches the availability of the host EC2 machine.
26/// ref. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-categories.html
27pub async fn fetch_availability_zone() -> Result<String> {
28    fetch_metadata_by_path("placement/availability-zone").await
29}
30
31/// Fetches the spot instance action.
32///
33/// If Amazon EC2 is not stopping or terminating the instance, or if you terminated the instance yourself,
34/// spot/instance-action is not present in the instance metadata thus returning an HTTP 404 error.
35///
36/// ref. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-categories.html
37/// ref. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/prepare-for-interruptions.html
38pub async fn fetch_spot_instance_action() -> Result<InstanceAction> {
39    let resp = fetch_metadata_by_path("spot/instance-action").await?;
40    serde_json::from_slice(resp.as_bytes()).map_err(|e| Error::Other {
41        message: format!(
42            "failed to parse spot/instance-action response '{}' {:?}",
43            resp, e
44        ),
45        retryable: false,
46    })
47}
48
49/// ref. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-instance-termination-notices.html#instance-action-metadata
50/// ref. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-categories.html
51#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
52#[serde(rename_all = "snake_case")]
53pub struct InstanceAction {
54    pub action: String,
55    #[serde(with = "rfc_manager::serde_format::rfc_3339")]
56    pub time: DateTime<Utc>,
57}
58
59/// Fetches the region of the host EC2 machine.
60/// TODO: fix this...
61/// ref. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-categories.html
62pub async fn fetch_region() -> Result<String> {
63    let mut az = fetch_availability_zone().await?;
64    az.truncate(az.len() - 1);
65    Ok(az)
66}
67
68/// Fetches instance metadata service v2 with the "path".
69/// ref. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-categories.html
70/// ref. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html
71/// ref. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
72/// e.g., curl -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/public-ipv4
73pub async fn fetch_metadata_by_path(path: &str) -> Result<String> {
74    log::info!("fetching meta-data/{}", path);
75
76    let token = fetch_token().await?;
77
78    let uri = format!("http://169.254.169.254/latest/meta-data/{}", path);
79    let cli = ClientBuilder::new()
80        .user_agent(env!("CARGO_PKG_NAME"))
81        .danger_accept_invalid_certs(true)
82        .timeout(Duration::from_secs(15))
83        .connection_verbose(true)
84        .build()
85        .map_err(|e| Error::API {
86            message: format!("failed ClientBuilder build {:?}", e),
87            retryable: false,
88        })?;
89    let resp = cli
90        .get(&uri)
91        .header("X-aws-ec2-metadata-token", token)
92        .send()
93        .await
94        .map_err(|e| Error::API {
95            message: format!("failed to build GET meta-data/{} {:?}", path, e),
96            retryable: false,
97        })?;
98    let out = resp.bytes().await.map_err(|e| Error::API {
99        message: format!("failed to read bytes {:?}", e),
100        retryable: false,
101    })?;
102    let out: Vec<u8> = out.into();
103
104    match String::from_utf8(out) {
105        Ok(text) => Ok(text),
106        Err(e) => Err(Error::API {
107            message: format!("GET meta-data/{} failed String::from_utf8 ({})", path, e),
108            retryable: false,
109        }),
110    }
111}
112
113/// Serves session token for instance metadata service v2.
114/// ref. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-categories.html
115/// ref. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
116/// e.g., curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"
117const IMDS_V2_SESSION_TOKEN_URI: &str = "http://169.254.169.254/latest/api/token";
118
119/// Fetches the IMDS v2 token.
120async fn fetch_token() -> Result<String> {
121    log::info!("fetching IMDS v2 token");
122
123    let cli = ClientBuilder::new()
124        .user_agent(env!("CARGO_PKG_NAME"))
125        .danger_accept_invalid_certs(true)
126        .timeout(Duration::from_secs(15))
127        .connection_verbose(true)
128        .build()
129        .map_err(|e| Error::API {
130            message: format!("failed ClientBuilder build {:?}", e),
131            retryable: false,
132        })?;
133    let resp = cli
134        .put(IMDS_V2_SESSION_TOKEN_URI)
135        .header("X-aws-ec2-metadata-token-ttl-seconds", "21600")
136        .send()
137        .await
138        .map_err(|e| Error::API {
139            message: format!("failed to build PUT api/token {:?}", e),
140            retryable: false,
141        })?;
142    let out = resp.bytes().await.map_err(|e| Error::API {
143        message: format!("failed to read bytes {:?}", e),
144        retryable: false,
145    })?;
146    let out: Vec<u8> = out.into();
147
148    match String::from_utf8(out) {
149        Ok(text) => Ok(text),
150        Err(e) => Err(Error::API {
151            message: format!("GET token failed String::from_utf8 ({})", e),
152            retryable: false,
153        }),
154    }
155}