cloud_detect/providers/
openstack.rs

1//! OpenStack.
2
3use std::path::Path;
4use std::time::Duration;
5
6use async_trait::async_trait;
7use tokio::fs;
8use tokio::sync::mpsc::Sender;
9use tracing::{debug, error, info, instrument};
10
11use crate::{Provider, ProviderId};
12
13const METADATA_URI: &str = "http://169.254.169.254";
14const METADATA_PATH: &str = "/openstack/";
15const PRODUCT_NAME_FILE: &str = "/sys/class/dmi/id/product_name";
16const PRODUCT_NAMES: [&str; 2] = ["Openstack Nova", "OpenStack Compute"];
17const CHASSIS_ASSET_TAG_FILE: &str = "/sys/class/dmi/id/chassis_asset_tag";
18const CHASSIS_ASSET_TAGS: [&str; 5] = [
19    "HUAWEICLOUD",
20    "OpenTelekomCloud",
21    "SAP CCloud VM",
22    "OpenStack Nova",
23    "OpenStack Compute",
24];
25pub(crate) const IDENTIFIER: ProviderId = ProviderId::OpenStack;
26
27pub(crate) struct OpenStack;
28
29#[async_trait]
30impl Provider for OpenStack {
31    fn identifier(&self) -> ProviderId {
32        IDENTIFIER
33    }
34
35    /// Tries to identify OpenStack using all the implemented options.
36    #[instrument(skip_all)]
37    async fn identify(&self, tx: Sender<ProviderId>, timeout: Duration) {
38        info!("Checking OpenStack");
39        if self
40            .check_vendor_files(PRODUCT_NAME_FILE, CHASSIS_ASSET_TAG_FILE)
41            .await
42            || self.check_metadata_server(METADATA_URI, timeout).await
43        {
44            info!("Identified OpenStack");
45            let res = tx.send(IDENTIFIER).await;
46
47            if let Err(err) = res {
48                error!("Error sending message: {:?}", err);
49            }
50        }
51    }
52}
53
54impl OpenStack {
55    /// Tries to identify OpenStack via metadata server.
56    #[instrument(skip_all)]
57    async fn check_metadata_server(&self, metadata_uri: &str, timeout: Duration) -> bool {
58        let url = format!("{metadata_uri}{METADATA_PATH}");
59        debug!("Checking {} metadata using url: {}", IDENTIFIER, url);
60
61        let client = if let Ok(client) = reqwest::Client::builder().timeout(timeout).build() {
62            client
63        } else {
64            error!("Error creating client");
65            return false;
66        };
67
68        match client.get(url).send().await {
69            Ok(resp) => resp.status().is_success(),
70            Err(err) => {
71                error!("Error making request: {:?}", err);
72                false
73            }
74        }
75    }
76
77    /// Tries to identify OpenStack using vendor file(s).
78    #[instrument(skip_all)]
79    async fn check_vendor_files<P: AsRef<Path>>(
80        &self,
81        product_name_file: P,
82        chassis_asset_tag_file: P,
83    ) -> bool {
84        debug!(
85            "Checking {} vendor file: {}",
86            IDENTIFIER,
87            product_name_file.as_ref().display()
88        );
89
90        if product_name_file.as_ref().is_file() {
91            match fs::read_to_string(product_name_file).await {
92                Ok(content) => {
93                    if PRODUCT_NAMES.iter().any(|&name| content.contains(name)) {
94                        return true;
95                    }
96                }
97                Err(err) => {
98                    error!("Error reading file: {:?}", err);
99                }
100            }
101        }
102
103        debug!(
104            "Checking {} vendor file: {}",
105            IDENTIFIER,
106            chassis_asset_tag_file.as_ref().display(),
107        );
108
109        if chassis_asset_tag_file.as_ref().is_file() {
110            match fs::read_to_string(chassis_asset_tag_file).await {
111                Ok(content) => {
112                    if CHASSIS_ASSET_TAGS
113                        .iter()
114                        .any(|&name| content.contains(name))
115                    {
116                        return true;
117                    }
118                }
119                Err(err) => {
120                    error!("Error reading file: {:?}", err);
121                }
122            }
123        }
124
125        false
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use std::io::Write;
132
133    use anyhow::Result;
134    use tempfile::NamedTempFile;
135    use wiremock::matchers::path;
136    use wiremock::{Mock, MockServer, ResponseTemplate};
137
138    use super::*;
139
140    #[tokio::test]
141    async fn test_check_metadata_server_success() {
142        let mock_server = MockServer::start().await;
143        Mock::given(path(METADATA_PATH))
144            .respond_with(ResponseTemplate::new(200))
145            .expect(1)
146            .mount(&mock_server)
147            .await;
148
149        let provider = OpenStack;
150        let metadata_uri = mock_server.uri();
151        let result = provider
152            .check_metadata_server(&metadata_uri, Duration::from_secs(1))
153            .await;
154
155        assert!(result);
156    }
157
158    #[tokio::test]
159    async fn test_check_metadata_server_failure() {
160        let mock_server = MockServer::start().await;
161        Mock::given(path(METADATA_PATH))
162            .respond_with(ResponseTemplate::new(500))
163            .expect(1)
164            .mount(&mock_server)
165            .await;
166
167        let provider = OpenStack;
168        let metadata_uri = mock_server.uri();
169        let result = provider
170            .check_metadata_server(&metadata_uri, Duration::from_secs(1))
171            .await;
172
173        assert!(!result);
174    }
175
176    #[tokio::test]
177    async fn test_check_vendor_file_success() -> Result<()> {
178        let mut product_name_file = NamedTempFile::new()?;
179        let mut chassis_asset_tag_file = NamedTempFile::new()?;
180
181        product_name_file.write_all(PRODUCT_NAMES[0].as_bytes())?;
182        chassis_asset_tag_file.write_all(CHASSIS_ASSET_TAGS[0].as_bytes())?;
183
184        let provider = OpenStack;
185        let result = provider
186            .check_vendor_files(product_name_file.path(), chassis_asset_tag_file.path())
187            .await;
188
189        assert!(result);
190
191        Ok(())
192    }
193
194    #[tokio::test]
195    async fn test_check_vendor_file_failure() -> Result<()> {
196        let product_name_file = NamedTempFile::new()?;
197        let chassis_asset_tag_file = NamedTempFile::new()?;
198
199        let provider = OpenStack;
200        let result = provider
201            .check_vendor_files(product_name_file.path(), chassis_asset_tag_file.path())
202            .await;
203
204        assert!(!result);
205
206        Ok(())
207    }
208}