cloud_detect/providers/
oci.rs

1//! Oracle Cloud Infrastructure (OCI).
2
3use std::path::Path;
4use std::time::Duration;
5
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use tokio::fs;
9use tokio::sync::mpsc::Sender;
10use tracing::{debug, error, info, instrument};
11
12use crate::{Provider, ProviderId};
13
14const METADATA_URI: &str = "http://169.254.169.254";
15const METADATA_PATH: &str = "/opc/v1/instance/metadata/";
16const VENDOR_FILE: &str = "/sys/class/dmi/id/chassis_asset_tag";
17pub(crate) const IDENTIFIER: ProviderId = ProviderId::OCI;
18
19#[derive(Serialize, Deserialize)]
20struct MetadataResponse {
21    #[serde(rename = "oke-tm")]
22    oke_tm: String,
23}
24
25pub(crate) struct Oci;
26
27#[async_trait]
28impl Provider for Oci {
29    fn identifier(&self) -> ProviderId {
30        IDENTIFIER
31    }
32
33    /// Tries to identify OCI using all the implemented options.
34    #[instrument(skip_all)]
35    async fn identify(&self, tx: Sender<ProviderId>, timeout: Duration) {
36        info!("Checking Oracle Cloud Infrastructure");
37        if self.check_vendor_file(VENDOR_FILE).await
38            || self.check_metadata_server(METADATA_URI, timeout).await
39        {
40            info!("Identified Oracle Cloud Infrastructure");
41            let res = tx.send(IDENTIFIER).await;
42
43            if let Err(err) = res {
44                error!("Error sending message: {:?}", err);
45            }
46        }
47    }
48}
49
50impl Oci {
51    /// Tries to identify OCI via metadata server.
52    #[instrument(skip_all)]
53    async fn check_metadata_server(&self, metadata_uri: &str, timeout: Duration) -> bool {
54        let url = format!("{metadata_uri}{METADATA_PATH}");
55        debug!("Checking {} metadata using url: {}", IDENTIFIER, url);
56
57        let client = if let Ok(client) = reqwest::Client::builder().timeout(timeout).build() {
58            client
59        } else {
60            error!("Error creating client");
61            return false;
62        };
63
64        match client.get(url).send().await {
65            Ok(resp) => match resp.json::<MetadataResponse>().await {
66                Ok(resp) => resp.oke_tm.contains("oke"),
67                Err(err) => {
68                    error!("Error reading response: {:?}", err);
69                    false
70                }
71            },
72            Err(err) => {
73                error!("Error making request: {:?}", err);
74                false
75            }
76        }
77    }
78
79    /// Tries to identify OCI using vendor file(s).
80    #[instrument(skip_all)]
81    async fn check_vendor_file<P: AsRef<Path>>(&self, vendor_file: P) -> bool {
82        debug!(
83            "Checking {} vendor file: {}",
84            IDENTIFIER,
85            vendor_file.as_ref().display()
86        );
87
88        if vendor_file.as_ref().is_file() {
89            return match fs::read_to_string(vendor_file).await {
90                Ok(content) => content.contains("OracleCloud"),
91                Err(err) => {
92                    error!("Error reading file: {:?}", err);
93                    false
94                }
95            };
96        }
97
98        false
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use std::io::Write;
105
106    use anyhow::Result;
107    use tempfile::NamedTempFile;
108    use wiremock::matchers::path;
109    use wiremock::{Mock, MockServer, ResponseTemplate};
110
111    use super::*;
112
113    #[tokio::test]
114    async fn test_check_metadata_server_success() {
115        let mock_server = MockServer::start().await;
116        Mock::given(path(METADATA_PATH))
117            .respond_with(ResponseTemplate::new(200).set_body_json(MetadataResponse {
118                oke_tm: "oke".to_string(),
119            }))
120            .expect(1)
121            .mount(&mock_server)
122            .await;
123
124        let provider = Oci;
125        let metadata_uri = mock_server.uri();
126        let result = provider
127            .check_metadata_server(&metadata_uri, Duration::from_secs(1))
128            .await;
129
130        assert!(result);
131    }
132
133    #[tokio::test]
134    async fn test_check_metadata_server_failure() {
135        let mock_server = MockServer::start().await;
136        Mock::given(path(METADATA_PATH))
137            .respond_with(ResponseTemplate::new(200).set_body_json(MetadataResponse {
138                oke_tm: "abc".to_string(),
139            }))
140            .expect(1)
141            .mount(&mock_server)
142            .await;
143
144        let provider = Oci;
145        let metadata_uri = mock_server.uri();
146        let result = provider
147            .check_metadata_server(&metadata_uri, Duration::from_secs(1))
148            .await;
149
150        assert!(!result);
151    }
152
153    #[tokio::test]
154    async fn test_check_vendor_file_success() -> Result<()> {
155        let mut vendor_file = NamedTempFile::new()?;
156        vendor_file.write_all(b"OracleCloud")?;
157
158        let provider = Oci;
159        let result = provider.check_vendor_file(vendor_file.path()).await;
160
161        assert!(result);
162
163        Ok(())
164    }
165
166    #[tokio::test]
167    async fn test_check_vendor_file_failure() -> Result<()> {
168        let vendor_file = NamedTempFile::new()?;
169
170        let provider = Oci;
171        let result = provider.check_vendor_file(vendor_file.path()).await;
172
173        assert!(!result);
174
175        Ok(())
176    }
177}