]> git.proxmox.com Git - proxmox-backup.git/blob - src/tools/disks/smart.rs
fb85f6670b6cf033ed3abdff9f491d1e5c143f8c
[proxmox-backup.git] / src / tools / disks / smart.rs
1 use std::collections::{HashMap, HashSet};
2
3 use lazy_static::lazy_static;
4 use anyhow::{bail, Error};
5 use ::serde::{Deserialize, Serialize};
6
7 use proxmox::api::api;
8
9 #[api()]
10 #[derive(Debug, Serialize, Deserialize)]
11 #[serde(rename_all="lowercase")]
12 /// SMART status
13 pub enum SmartStatus {
14 /// Smart tests passed - everything is OK
15 Passed,
16 /// Smart tests failed - disk has problems
17 Failed,
18 /// Unknown status
19 Unknown,
20 }
21
22 #[api()]
23 #[derive(Debug, Serialize, Deserialize)]
24 /// SMART Attribute
25 pub struct SmartAttribute {
26 /// Attribute name
27 name: String,
28 /// Attribute raw value
29 value: String,
30 // the rest of the values is available for ATA type
31 /// ATA Attribute ID
32 #[serde(skip_serializing_if="Option::is_none")]
33 id: Option<u64>,
34 /// ATA Flags
35 #[serde(skip_serializing_if="Option::is_none")]
36 flags: Option<String>,
37 /// ATA normalized value (0..100)
38 #[serde(skip_serializing_if="Option::is_none")]
39 normalized: Option<f64>,
40 /// ATA worst
41 #[serde(skip_serializing_if="Option::is_none")]
42 worst: Option<f64>,
43 /// ATA threshold
44 #[serde(skip_serializing_if="Option::is_none")]
45 threshold: Option<f64>,
46 }
47
48
49 #[api(
50 properties: {
51 status: {
52 type: SmartStatus,
53 },
54 wearout: {
55 description: "Wearout level.",
56 type: f64,
57 optional: true,
58 },
59 attributes: {
60 description: "SMART attributes.",
61 type: Array,
62 items: {
63 type: SmartAttribute,
64 },
65 },
66 },
67 )]
68 #[derive(Debug, Serialize, Deserialize)]
69 /// Data from smartctl
70 pub struct SmartData {
71 pub status: SmartStatus,
72 pub wearout: Option<f64>,
73 pub attributes: Vec<SmartAttribute>,
74 }
75
76 /// Read smartctl data for a disk (/dev/XXX).
77 pub fn get_smart_data(
78 disk: &super::Disk,
79 health_only: bool,
80 ) -> Result<SmartData, Error> {
81
82 const SMARTCTL_BIN_PATH: &str = "smartctl";
83
84 let mut command = std::process::Command::new(SMARTCTL_BIN_PATH);
85 command.arg("-H");
86 if !health_only { command.args(&["-A", "-j"]); }
87
88 let disk_path = match disk.device_path() {
89 Some(path) => path,
90 None => bail!("disk {:?} has no node in /dev", disk.syspath()),
91 };
92 command.arg(disk_path);
93
94 let output = pbs_tools::run_command(command, Some(|exitcode|
95 (exitcode & 0b0111) == 0 // only bits 0-2 are fatal errors
96 ))?;
97
98 let output: serde_json::Value = output.parse()?;
99
100 let mut wearout = None;
101
102 let mut attributes = Vec::new();
103 let mut wearout_candidates = HashMap::new();
104
105 // ATA devices
106 if let Some(list) = output["ata_smart_attributes"]["table"].as_array() {
107 for item in list {
108 let id = match item["id"].as_u64() {
109 Some(id) => id,
110 None => continue, // skip attributes without id
111 };
112
113 let name = match item["name"].as_str() {
114 Some(name) => name.to_string(),
115 None => continue, // skip attributes without name
116 };
117
118 let raw_value = match item["raw"]["string"].as_str() {
119 Some(value) => value.to_string(),
120 None => continue, // skip attributes without raw value
121 };
122
123 let flags = match item["flags"]["string"].as_str() {
124 Some(flags) => flags.to_string(),
125 None => continue, // skip attributes without flags
126 };
127
128 let normalized = match item["value"].as_f64() {
129 Some(v) => v,
130 None => continue, // skip attributes without normalize value
131 };
132
133 let worst = match item["worst"].as_f64() {
134 Some(v) => v,
135 None => continue, // skip attributes without worst entry
136 };
137
138 let threshold = match item["thresh"].as_f64() {
139 Some(v) => v,
140 None => continue, // skip attributes without threshold entry
141 };
142
143 if WEAROUT_FIELD_NAMES.contains(&name as &str) {
144 wearout_candidates.insert(name.clone(), normalized);
145 }
146
147 attributes.push(SmartAttribute {
148 name,
149 value: raw_value,
150 id: Some(id),
151 flags: Some(flags),
152 normalized: Some(normalized),
153 worst: Some(worst),
154 threshold: Some(threshold),
155 });
156 }
157 }
158
159 if !wearout_candidates.is_empty() {
160 for field in WEAROUT_FIELD_ORDER {
161 if let Some(value) = wearout_candidates.get(field as &str) {
162 wearout = Some(*value);
163 break;
164 }
165 }
166 }
167
168 // NVME devices
169 if let Some(list) = output["nvme_smart_health_information_log"].as_object() {
170 for (name, value) in list {
171 if name == "percentage_used" {
172 // extract wearout from nvme text, allow for decimal values
173 if let Some(v) = value.as_f64() {
174 if v <= 100.0 {
175 wearout = Some(100.0 - v);
176 }
177 }
178 }
179 if let Some(value) = value.as_f64() {
180 attributes.push(SmartAttribute {
181 name: name.to_string(),
182 value: value.to_string(),
183 id: None,
184 flags: None,
185 normalized: None,
186 worst: None,
187 threshold: None,
188 });
189 }
190 }
191 }
192
193 let status = match output["smart_status"]["passed"].as_bool() {
194 None => SmartStatus::Unknown,
195 Some(true) => SmartStatus::Passed,
196 Some(false) => SmartStatus::Failed,
197 };
198
199
200 Ok(SmartData { status, wearout, attributes })
201 }
202
203 static WEAROUT_FIELD_ORDER: &[&'static str] = &[
204 "Media_Wearout_Indicator",
205 "SSD_Life_Left",
206 "Wear_Leveling_Count",
207 "Perc_Write/Erase_Ct_BC",
208 "Perc_Rated_Life_Remain",
209 "Remaining_Lifetime_Perc",
210 "Percent_Lifetime_Remain",
211 "Lifetime_Left",
212 "PCT_Life_Remaining",
213 "Lifetime_Remaining",
214 "Percent_Life_Remaining",
215 "Percent_Lifetime_Used",
216 "Perc_Rated_Life_Used"
217 ];
218
219 lazy_static! {
220 static ref WEAROUT_FIELD_NAMES: HashSet<&'static str> = {
221 WEAROUT_FIELD_ORDER.iter().cloned().collect()
222 };
223 }