Browse Source

Replaced tabs with spaces

tags/0.3.1
Johann Schmitz 2 years ago
parent
commit
199b5395f9
6 changed files with 692 additions and 692 deletions
  1. 13
    13
      setup.py
  2. 341
    341
      src/smartcheck/check.py
  3. 35
    35
      src/smartcheck/convert.py
  4. 77
    77
      src/smartcheck/main.py
  5. 141
    141
      tests/check.py
  6. 85
    85
      tests/parsing.py

+ 13
- 13
setup.py View File

@@ -4,17 +4,17 @@
4 4
 from setuptools import setup, find_packages
5 5
 
6 6
 setup(
7
-	name='smart-check',
8
-	version='0.3',
9
-	description='A smart S.M.A.R.T. check',
10
-	author='Johann Schmitz',
11
-	author_email='johann@j-schmitz.net',
12
-	url='https://ercpe.de/projects/smart-check',
13
-	download_url='https://code.not-your-server.de/smart-check.git/tags/',
14
-	packages=find_packages('src'),
15
-	package_dir={'': 'src'},
16
-	include_package_data=True,
17
-	package_data = {'': ['*.yaml']},
18
-	zip_safe=False,
19
-	license='GPL-3',
7
+    name='smart-check',
8
+    version='0.3',
9
+    description='A smart S.M.A.R.T. check',
10
+    author='Johann Schmitz',
11
+    author_email='johann@j-schmitz.net',
12
+    url='https://ercpe.de/projects/smart-check',
13
+    download_url='https://code.not-your-server.de/smart-check.git/tags/',
14
+    packages=find_packages('src'),
15
+    package_dir={'': 'src'},
16
+    include_package_data=True,
17
+    package_data = {'': ['*.yaml']},
18
+    zip_safe=False,
19
+    license='GPL-3',
20 20
 )

+ 341
- 341
src/smartcheck/check.py View File

@@ -14,16 +14,16 @@ TESTS_SECTION_START = 'SMART Self-test log structure revision number'
14 14
 ATA_ERROR_COUNT = re.compile('^ATA Error Count: (\d+).*', re.MULTILINE | re.IGNORECASE)
15 15
 
16 16
 INFORMATION_RE = [
17
-	("model_family", re.compile('Model Family: (.*)', re.UNICODE)),
18
-	("device_model", re.compile("(?:Device Model|Product): (.*)", re.UNICODE)),
19
-	("serial", re.compile("Serial Number: (.*)", re.UNICODE | re.IGNORECASE)),
20
-	("firmware_version", re.compile("Firmware version: (.*)", re.UNICODE)),
21
-	("ata_version", re.compile("ATA Version is: (.*)", re.UNICODE)),
22
-	("sata_version", re.compile("SATA Version is: (.*)", re.UNICODE)),
17
+    ("model_family", re.compile('Model Family: (.*)', re.UNICODE)),
18
+    ("device_model", re.compile("(?:Device Model|Product): (.*)", re.UNICODE)),
19
+    ("serial", re.compile("Serial Number: (.*)", re.UNICODE | re.IGNORECASE)),
20
+    ("firmware_version", re.compile("Firmware version: (.*)", re.UNICODE)),
21
+    ("ata_version", re.compile("ATA Version is: (.*)", re.UNICODE)),
22
+    ("sata_version", re.compile("SATA Version is: (.*)", re.UNICODE)),
23 23
 ]
24 24
 
25 25
 DATA_RE = [
26
-	('overall_health_status', re.compile('SMART overall-health self-assessment test result: (.*)', re.UNICODE)),
26
+    ('overall_health_status', re.compile('SMART overall-health self-assessment test result: (.*)', re.UNICODE)),
27 27
 ]
28 28
 DATA_ATTRIBUTES_RE = re.compile(r"\s*(\d+)\s+([\w\d_\-]+)\s+([0-9a-fx]+)\s+(\d+)\s+(\d+)\s+(\d+)\s+([\w\d_\-]+)\s+([\w\d]+)\s+([\w\d_\-]+)\s+([^\r\n]*)", re.UNICODE)
29 29
 
@@ -31,352 +31,352 @@ TEST_RESULT_RE = re.compile(r"#\s*(\d+)\s+(.*?)\s{2,}(.*?)\s{2,}\s+([\d%]+)\s+(\
31 31
 
32 32
 
33 33
 def toint(s, default=0):
34
-	try:
35
-		return int(s)
36
-	except ValueError:
37
-		return default
34
+    try:
35
+        return int(s)
36
+    except ValueError:
37
+        return default
38 38
 
39 39
 
40 40
 class AttributeWarning(object):
41
-	Notice = 'NOTICE'
42
-	Warning = 'WARNING'
43
-	Critical = 'CRITICAL'
41
+    Notice = 'NOTICE'
42
+    Warning = 'WARNING'
43
+    Critical = 'CRITICAL'
44 44
 
45
-	def __init__(self, level=None, attribute_name=None, value=None, description=None):
46
-		self.level = level
47
-		self.field = attribute_name
48
-		self.value = value
49
-		self.description = (description or '').strip()
45
+    def __init__(self, level=None, attribute_name=None, value=None, description=None):
46
+        self.level = level
47
+        self.field = attribute_name
48
+        self.value = value
49
+        self.description = (description or '').strip()
50 50
 
51
-	@property
52
-	def short_message(self):
53
-		return "%s: %s=%s" % (self.level or '?', self.field, self.value)
51
+    @property
52
+    def short_message(self):
53
+        return "%s: %s=%s" % (self.level or '?', self.field, self.value)
54 54
 
55
-	@property
56
-	def long_message(self):
57
-		s = self.short_message
55
+    @property
56
+    def long_message(self):
57
+        s = self.short_message
58 58
 
59
-		if self.description:
60
-			s += ": %s" % self.description
59
+        if self.description:
60
+            s += ": %s" % self.description
61 61
 
62
-		return s
62
+        return s
63 63
 
64
-	def __str__(self):
65
-		return self.short_message
64
+    def __str__(self):
65
+        return self.short_message
66 66
 
67
-	def __repr__(self):
68
-		return self.short_message
67
+    def __repr__(self):
68
+        return self.short_message
69 69
 
70
-	def __eq__(self, other):
71
-		return isinstance(other, AttributeWarning) and \
72
-					self.level is not None and self.level == other.level and \
73
-					self.field is not None and self.field == other.field and \
74
-					self.value is not None and self.value == other.value
70
+    def __eq__(self, other):
71
+        return isinstance(other, AttributeWarning) and \
72
+                    self.level is not None and self.level == other.level and \
73
+                    self.field is not None and self.field == other.field and \
74
+                    self.value is not None and self.value == other.value
75 75
 
76 76
 
77 77
 class SMARTCheck(object):
78 78
 
79
-	def __init__(self, file_or_string, db_path=None):
80
-		if hasattr(file_or_string, 'read'):
81
-			self.raw = file_or_string.read()
82
-		elif isinstance(file_or_string, str) or (sys.version_info[0] == 2 and isinstance(file_or_string, unicode)):
83
-			self.raw = file_or_string
84
-		elif isinstance(file_or_string, bytes):
85
-			self.raw = file_or_string.decode('UTF-8')
86
-		else:
87
-			raise Exception("Unknown type: %s" % type(file_or_string))
88
-
89
-		self.parsed_sections = None
90
-		self.db_path = db_path
91
-		self._database = None
92
-
93
-	@property
94
-	def information(self):
95
-		return self.parsed.get('information', {})
96
-
97
-	@property
98
-	def smart_data(self):
99
-		return self.parsed.get('data', {})
100
-
101
-	@property
102
-	def self_tests(self):
103
-		return self.parsed.get('self_tests', {})
104
-
105
-	@property
106
-	def parsed(self):
107
-		if not self.parsed_sections:
108
-			self.parsed_sections = self.parse()
109
-		return self.parsed_sections
110
-
111
-	@property
112
-	def database(self):
113
-		if self._database is None:
114
-			if self.db_path:
115
-				with open(self.db_path) as f:
116
-					self._database = yaml.load(f) or {}
117
-			else:
118
-				self._database = []
119
-		return self._database
120
-
121
-	@property
122
-	def device_model(self):
123
-		return self.information['device_model']
124
-
125
-	@property
126
-	def ata_error_count(self):
127
-		return self.parsed['ata_error_count']
128
-
129
-	def exists_in_database(self):
130
-		return self.get_attributes_from_database(self.device_model) is not None
131
-
132
-	def get_attributes_from_database(self, device_model):
133
-		for dev in self.database:
134
-			device_regexprs = dev['model'] if isinstance(dev['model'], list) else [dev['model']]
135
-			if any(re.match(r, device_model, re.IGNORECASE) for r in device_regexprs):
136
-				logging.debug("Device exists in database (one of %s matches %s)" % (device_regexprs, self.device_model))
137
-				return dev['attributes']
138
-		logging.debug("Device does not exist in database")
139
-		return None
140
-
141
-	def parse(self):
142
-		return {
143
-			'information': self.parse_information_section(self.raw),
144
-			'data': self.parse_data_section(self.raw),
145
-			'self_tests': self.parse_tests_section(self.raw),
146
-			'ata_error_count': self.parse_ata_error_count(self.raw),
147
-		}
148
-
149
-	@property
150
-	def data_parsed(self):
151
-		return 'attributes' in self.smart_data
152
-
153
-	def parse_information_section(self, s):
154
-		if INFORMATION_SECTION_START not in s:
155
-			return {}
156
-
157
-		start = s.index(INFORMATION_SECTION_START)
158
-
159
-		if DATA_SECTION_START not in s:
160
-			end = len(s)
161
-		else:
162
-			end = s.index(DATA_SECTION_START)
163
-
164
-		information_text = s[start:end]
165
-
166
-		d = {}
167
-		for k, regex in INFORMATION_RE:
168
-			m = regex.search(information_text)
169
-			if m:
170
-				d[k] = m.group(1).strip() if m.group(1) else ''
171
-		return d
172
-
173
-	def parse_data_section(self, s):
174
-		if DATA_SECTION_START not in s:
175
-			logging.info("No data section found")
176
-			return {}
177
-
178
-		start = s.index(DATA_SECTION_START)
179
-		data_text = s[start:]
180
-
181
-		d = {}
182
-		for k, regex in DATA_RE:
183
-			m = regex.search(data_text)
184
-			if m:
185
-				d[k] = m.group(1).strip() if m.group(1) else ''
186
-
187
-		d['attributes'] = sorted(DATA_ATTRIBUTES_RE.findall(s), key=lambda t: int(t[0]))
188
-
189
-		return d
190
-
191
-	def parse_tests_section(self, s):
192
-		if TESTS_SECTION_START not in s:
193
-			return {
194
-				'test_results': []
195
-			}
196
-
197
-		start = s.index(TESTS_SECTION_START)
198
-		end = re.search(r'(\r\n\r\n|\n\n|\r\r)', s[start+1:], re.MULTILINE)
199
-		end = start + end.end(0) if end else len(s)
200
-
201
-		tests_text = s[start:end]
202
-
203
-		return {
204
-			'test_results': TEST_RESULT_RE.findall(tests_text)
205
-		}
206
-
207
-	def parse_ata_error_count(self, s):
208
-		m = ATA_ERROR_COUNT.search(s)
209
-		if m:
210
-			return int(m.group(1))
211
-		return 0
212
-
213
-	def check(self, ignore_attributes=None):
214
-		return len(self.check_attributes(ignore_attributes or [])) == 0 and self.check_tests() and self.ata_error_count == 0
215
-
216
-	def check_tests(self):
217
-		ok_test_results = [
218
-			'Completed without error',
219
-			'Interrupted (host reset)', # reboot during self test
220
-			'Aborted by host'
221
-		]
222
-		return not any([x[2] not in ok_test_results for x in self.self_tests['test_results']])
223
-
224
-	def check_attributes(self, ignore_attributes=None):
225
-		failed_attributes = self.check_generic_attributes()
226
-
227
-		if self.exists_in_database():
228
-			failed_attributes.update(self.check_device_attributes())
229
-
230
-		# remove every AttributeWarning from failed_attributes based on ignore_attributes
231
-		for attr_id_or_name in ignore_attributes or []:
232
-			del_keys = []
233
-			if isinstance(attr_id_or_name, int) or attr_id_or_name.isdigit():
234
-				del_keys = [k for k in failed_attributes.keys() if k[0] == int(attr_id_or_name)]
235
-			else:
236
-				del_keys = [k for k in failed_attributes.keys() if k[1] == attr_id_or_name]
237
-
238
-			for x in del_keys:
239
-				del failed_attributes[x]
240
-
241
-		return failed_attributes
242
-
243
-	def check_generic_attributes(self):
244
-		failed_attributes = {}
245
-
246
-		for attrid, name, flag, value, worst, thresh, attr_type, updated, when_failed, raw_value in self.smart_data['attributes']:
247
-			logging.debug("Attribute %s (%s): value=%s, raw value=%s" % (attrid, name, value, raw_value))
248
-			attrid = int(attrid)
249
-			attr_name = (name or '').lower()
250
-			int_value = toint(value)
251
-			int_raw_value = toint(raw_value)
252
-			int_thresh = toint(thresh)
253
-
254
-			# these tests are take from gsmartcontrol (storage_property_descr.cpp) and check for known pre-fail attributes
255
-			if attr_name in ('reallocated_sector_count', 'reallocated_sector_ct') and int_raw_value > 0:
256
-				failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Notice,
257
-																	 name,
258
-																	 raw_value,
259
-																	 "The drive has a non-zero Raw value, but there is no SMART warning yet. " +
260
-																	 "This could be an indication of future failures and/or potential data loss in bad sectors.")
261
-			elif attr_name == 'spin_up_retry_count' and int_raw_value > 0:
262
-				failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Notice,
263
-																	 name,
264
-																	 raw_value,
265
-																	 "The drive has a non-zero Raw value, but there is no SMART warning yet. " +
266
-																	 "Your drive may have problems spinning up, which could lead to a complete mechanical failure.")
267
-			elif attr_name == "soft_read_error_rate" and int_raw_value > 0:
268
-				failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Notice,
269
-																	 name,
270
-																	 raw_value,
271
-																	 "The drive has a non-zero Raw value, but there is no SMART warning yet. " +
272
-																	 "This could be an indication of future failures and/or potential data loss in bad sectors.")
273
-			elif attr_name in ("temperature_celsius", "temperature_celsius_x10"):
274
-				if 50 <= int_raw_value <= 120:
275
-					# Temperature (for some it may be 10xTemp, so limit the upper bound.)
276
-					failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Notice,
277
-																		 name,
278
-																		 int_raw_value,
279
-																		 "The temperature of the drive is higher than 50 degrees Celsius. " +
280
-																		 "This may shorten its lifespan and cause damage under severe load.")
281
-				elif int_raw_value > 500:
282
-					# Temperature (for some it may be 10xTemp, so limit the upper bound.)
283
-					failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Notice,
284
-																		 name,
285
-																		 int_raw_value,
286
-																		 "The temperature of the drive is higher than 50 degrees Celsius. " +
287
-																		 "This may shorten its lifespan and cause damage under severe load.")
288
-			elif attr_name == "reallocation_event_count" and int_raw_value > 0:
289
-				failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Notice,
290
-																	 name,
291
-																	 raw_value,
292
-																	 "The drive has a non-zero Raw value, but there is no SMART warning yet. " +
293
-																	 "This could be an indication of future failures and/or potential data loss in bad sectors.")
294
-			elif attr_name in ("current_pending_sector", "current_pending_sector_count", "total_pending_sectors") and int_raw_value > 0:
295
-				failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Notice,
296
-																	 name,
297
-																	 raw_value,
298
-																	 "The drive has a non-zero Raw value, but there is no SMART warning yet. " +
299
-																	 "This could be an indication of future failures and/or potential data loss in bad sectors.")
300
-			elif attr_name in ("offline_uncorrectable", "total_offline_uncorrectable") and int_raw_value > 0:
301
-				failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Notice,
302
-																	 name,
303
-																	 raw_value,
304
-																	 "The drive has a non-zero Raw value, but there is no SMART warning yet. " +
305
-																	 "This could be an indication of future failures and/or potential data loss in bad sectors.")
306
-			elif attr_name == "ssd_life_left" and int_value < 50:
307
-				failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Notice,
308
-																	 name,
309
-																	 raw_value,
310
-																	 "The drive has less than half of its life left.")
311
-			else:
312
-				# execute a generic check for value < threshold
313
-				if int_value and int_thresh:
314
-					if int_value < int_thresh:
315
-						failed_attributes[(attrid, name)] = AttributeWarning(
316
-																	AttributeWarning.Warning if attr_type == 'Pre-fail' else AttributeWarning.Notice,
317
-																	name,
318
-																	raw_value,
319
-																	"Attribute value dropped below threshold of %s" % int_thresh)
320
-
321
-
322
-		logging.debug("Failed generic attributes: %s" % (failed_attributes, ))
323
-		return failed_attributes
324
-
325
-	def check_device_attributes(self):
326
-		device_model = self.device_model
327
-		device_db_attributes = self.get_attributes_from_database(device_model)
328
-
329
-		threshold_from = re.compile('^(\d+):$')
330
-		threshold_to = re.compile('^:(\d+)$')
331
-		threshold_from_to = re.compile('^(\d+):(\d+)$')
332
-
333
-		failed_attributes = {}
334
-
335
-		for attrid, name, flag, value, worst, tresh, type, updated, when_failed, raw_value in self.smart_data['attributes']:
336
-			attrid = int(attrid)
337
-			if attrid in device_db_attributes:
338
-				db_attrs = device_db_attributes[attrid]
339
-
340
-				if isinstance(db_attrs, list):
341
-					value_field, min_value, max_value = tuple(device_db_attributes[int(attrid)])
342
-
343
-					check_value = value if value_field == "VALUE" else raw_value
344
-					check_value = int(check_value or -1)
345
-
346
-					if not (int(min_value) <= check_value <= int(max_value)):
347
-						logging.info("Attribute %s (%s) failed: not %s <= %s <= %s" % (attrid, name, min_value, check_value, max_value))
348
-						failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Critical, name, check_value)
349
-				elif isinstance(db_attrs, dict):
350
-					value_field = db_attrs.get('field', 'RAW_VALUE')
351
-					check_value = value if value_field == "VALUE" else raw_value
352
-					check_value = int(check_value or -1)
353
-
354
-					min_value = db_attrs.get('min', None)
355
-					max_value = db_attrs.get('max', None)
356
-
357
-					if min_value is None and max_value is None:
358
-						for failure_type, threshold_key in [('WARNING', 'warn_threshold'), ('CRITICAL', 'crit_threshold')]:
359
-							if threshold_key not in db_attrs:
360
-								continue
361
-							v = db_attrs.get(threshold_key)
362
-
363
-							from_m = threshold_from.match(v)
364
-							to_m = threshold_to.match(v)
365
-							from_to_m = threshold_from_to.match(v)
366
-
367
-							if (from_m and check_value >= int(from_m.group(1))) or \
368
-								(to_m and check_value <= int(to_m.group(1))) or \
369
-								(from_to_m and (int(from_to_m.group(1)) <= check_value <= int(from_to_m.group(2)))):
370
-
371
-								logging.info("Attribute %s (%s) failed with %s: not within treshold %s" % (attrid, name, failure_type, v))
372
-								failed_attributes[(attrid, name)] = AttributeWarning(failure_type, name, check_value)
373
-					else:
374
-						if (min_value is not None and check_value >= int(min_value)) or \
375
-							(max_value is not None and check_value <= int(max_value)):
376
-							logging.info("Attribute %s (%s) failed: not %s >= %s <= %s" % (attrid, name, min_value, check_value, max_value))
377
-							failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Critical, name, check_value)
378
-
379
-				else:
380
-					raise ValueError("Unknown attribute specification: %s" % db_attrs)
381
-
382
-		return failed_attributes
79
+    def __init__(self, file_or_string, db_path=None):
80
+        if hasattr(file_or_string, 'read'):
81
+            self.raw = file_or_string.read()
82
+        elif isinstance(file_or_string, str) or (sys.version_info[0] == 2 and isinstance(file_or_string, unicode)):
83
+            self.raw = file_or_string
84
+        elif isinstance(file_or_string, bytes):
85
+            self.raw = file_or_string.decode('UTF-8')
86
+        else:
87
+            raise Exception("Unknown type: %s" % type(file_or_string))
88
+
89
+        self.parsed_sections = None
90
+        self.db_path = db_path
91
+        self._database = None
92
+
93
+    @property
94
+    def information(self):
95
+        return self.parsed.get('information', {})
96
+
97
+    @property
98
+    def smart_data(self):
99
+        return self.parsed.get('data', {})
100
+
101
+    @property
102
+    def self_tests(self):
103
+        return self.parsed.get('self_tests', {})
104
+
105
+    @property
106
+    def parsed(self):
107
+        if not self.parsed_sections:
108
+            self.parsed_sections = self.parse()
109
+        return self.parsed_sections
110
+
111
+    @property
112
+    def database(self):
113
+        if self._database is None:
114
+            if self.db_path:
115
+                with open(self.db_path) as f:
116
+                    self._database = yaml.load(f) or {}
117
+            else:
118
+                self._database = []
119
+        return self._database
120
+
121
+    @property
122
+    def device_model(self):
123
+        return self.information['device_model']
124
+
125
+    @property
126
+    def ata_error_count(self):
127
+        return self.parsed['ata_error_count']
128
+
129
+    def exists_in_database(self):
130
+        return self.get_attributes_from_database(self.device_model) is not None
131
+
132
+    def get_attributes_from_database(self, device_model):
133
+        for dev in self.database:
134
+            device_regexprs = dev['model'] if isinstance(dev['model'], list) else [dev['model']]
135
+            if any(re.match(r, device_model, re.IGNORECASE) for r in device_regexprs):
136
+                logging.debug("Device exists in database (one of %s matches %s)" % (device_regexprs, self.device_model))
137
+                return dev['attributes']
138
+        logging.debug("Device does not exist in database")
139
+        return None
140
+
141
+    def parse(self):
142
+        return {
143
+            'information': self.parse_information_section(self.raw),
144
+            'data': self.parse_data_section(self.raw),
145
+            'self_tests': self.parse_tests_section(self.raw),
146
+            'ata_error_count': self.parse_ata_error_count(self.raw),
147
+        }
148
+
149
+    @property
150
+    def data_parsed(self):
151
+        return 'attributes' in self.smart_data
152
+
153
+    def parse_information_section(self, s):
154
+        if INFORMATION_SECTION_START not in s:
155
+            return {}
156
+
157
+        start = s.index(INFORMATION_SECTION_START)
158
+
159
+        if DATA_SECTION_START not in s:
160
+            end = len(s)
161
+        else:
162
+            end = s.index(DATA_SECTION_START)
163
+
164
+        information_text = s[start:end]
165
+
166
+        d = {}
167
+        for k, regex in INFORMATION_RE:
168
+            m = regex.search(information_text)
169
+            if m:
170
+                d[k] = m.group(1).strip() if m.group(1) else ''
171
+        return d
172
+
173
+    def parse_data_section(self, s):
174
+        if DATA_SECTION_START not in s:
175
+            logging.info("No data section found")
176
+            return {}
177
+
178
+        start = s.index(DATA_SECTION_START)
179
+        data_text = s[start:]
180
+
181
+        d = {}
182
+        for k, regex in DATA_RE:
183
+            m = regex.search(data_text)
184
+            if m:
185
+                d[k] = m.group(1).strip() if m.group(1) else ''
186
+
187
+        d['attributes'] = sorted(DATA_ATTRIBUTES_RE.findall(s), key=lambda t: int(t[0]))
188
+
189
+        return d
190
+
191
+    def parse_tests_section(self, s):
192
+        if TESTS_SECTION_START not in s:
193
+            return {
194
+                'test_results': []
195
+            }
196
+
197
+        start = s.index(TESTS_SECTION_START)
198
+        end = re.search(r'(\r\n\r\n|\n\n|\r\r)', s[start+1:], re.MULTILINE)
199
+        end = start + end.end(0) if end else len(s)
200
+
201
+        tests_text = s[start:end]
202
+
203
+        return {
204
+            'test_results': TEST_RESULT_RE.findall(tests_text)
205
+        }
206
+
207
+    def parse_ata_error_count(self, s):
208
+        m = ATA_ERROR_COUNT.search(s)
209
+        if m:
210
+            return int(m.group(1))
211
+        return 0
212
+
213
+    def check(self, ignore_attributes=None):
214
+        return len(self.check_attributes(ignore_attributes or [])) == 0 and self.check_tests() and self.ata_error_count == 0
215
+
216
+    def check_tests(self):
217
+        ok_test_results = [
218
+            'Completed without error',
219
+            'Interrupted (host reset)', # reboot during self test
220
+            'Aborted by host'
221
+        ]
222
+        return not any([x[2] not in ok_test_results for x in self.self_tests['test_results']])
223
+
224
+    def check_attributes(self, ignore_attributes=None):
225
+        failed_attributes = self.check_generic_attributes()
226
+
227
+        if self.exists_in_database():
228
+            failed_attributes.update(self.check_device_attributes())
229
+
230
+        # remove every AttributeWarning from failed_attributes based on ignore_attributes
231
+        for attr_id_or_name in ignore_attributes or []:
232
+            del_keys = []
233
+            if isinstance(attr_id_or_name, int) or attr_id_or_name.isdigit():
234
+                del_keys = [k for k in failed_attributes.keys() if k[0] == int(attr_id_or_name)]
235
+            else:
236
+                del_keys = [k for k in failed_attributes.keys() if k[1] == attr_id_or_name]
237
+
238
+            for x in del_keys:
239
+                del failed_attributes[x]
240
+
241
+        return failed_attributes
242
+
243
+    def check_generic_attributes(self):
244
+        failed_attributes = {}
245
+
246
+        for attrid, name, flag, value, worst, thresh, attr_type, updated, when_failed, raw_value in self.smart_data['attributes']:
247
+            logging.debug("Attribute %s (%s): value=%s, raw value=%s" % (attrid, name, value, raw_value))
248
+            attrid = int(attrid)
249
+            attr_name = (name or '').lower()
250
+            int_value = toint(value)
251
+            int_raw_value = toint(raw_value)
252
+            int_thresh = toint(thresh)
253
+
254
+            # these tests are take from gsmartcontrol (storage_property_descr.cpp) and check for known pre-fail attributes
255
+            if attr_name in ('reallocated_sector_count', 'reallocated_sector_ct') and int_raw_value > 0:
256
+                failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Notice,
257
+                                                                     name,
258
+                                                                     raw_value,
259
+                                                                     "The drive has a non-zero Raw value, but there is no SMART warning yet. " +
260
+                                                                     "This could be an indication of future failures and/or potential data loss in bad sectors.")
261
+            elif attr_name == 'spin_up_retry_count' and int_raw_value > 0:
262
+                failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Notice,
263
+                                                                     name,
264
+                                                                     raw_value,
265
+                                                                     "The drive has a non-zero Raw value, but there is no SMART warning yet. " +
266
+                                                                     "Your drive may have problems spinning up, which could lead to a complete mechanical failure.")
267
+            elif attr_name == "soft_read_error_rate" and int_raw_value > 0:
268
+                failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Notice,
269
+                                                                     name,
270
+                                                                     raw_value,
271
+                                                                     "The drive has a non-zero Raw value, but there is no SMART warning yet. " +
272
+                                                                     "This could be an indication of future failures and/or potential data loss in bad sectors.")
273
+            elif attr_name in ("temperature_celsius", "temperature_celsius_x10"):
274
+                if 50 <= int_raw_value <= 120:
275
+                    # Temperature (for some it may be 10xTemp, so limit the upper bound.)
276
+                    failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Notice,
277
+                                                                         name,
278
+                                                                         int_raw_value,
279
+                                                                         "The temperature of the drive is higher than 50 degrees Celsius. " +
280
+                                                                         "This may shorten its lifespan and cause damage under severe load.")
281
+                elif int_raw_value > 500:
282
+                    # Temperature (for some it may be 10xTemp, so limit the upper bound.)
283
+                    failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Notice,
284
+                                                                         name,
285
+                                                                         int_raw_value,
286
+                                                                         "The temperature of the drive is higher than 50 degrees Celsius. " +
287
+                                                                         "This may shorten its lifespan and cause damage under severe load.")
288
+            elif attr_name == "reallocation_event_count" and int_raw_value > 0:
289
+                failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Notice,
290
+                                                                     name,
291
+                                                                     raw_value,
292
+                                                                     "The drive has a non-zero Raw value, but there is no SMART warning yet. " +
293
+                                                                     "This could be an indication of future failures and/or potential data loss in bad sectors.")
294
+            elif attr_name in ("current_pending_sector", "current_pending_sector_count", "total_pending_sectors") and int_raw_value > 0:
295
+                failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Notice,
296
+                                                                     name,
297
+                                                                     raw_value,
298
+                                                                     "The drive has a non-zero Raw value, but there is no SMART warning yet. " +
299
+                                                                     "This could be an indication of future failures and/or potential data loss in bad sectors.")
300
+            elif attr_name in ("offline_uncorrectable", "total_offline_uncorrectable") and int_raw_value > 0:
301
+                failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Notice,
302
+                                                                     name,
303
+                                                                     raw_value,
304
+                                                                     "The drive has a non-zero Raw value, but there is no SMART warning yet. " +
305
+                                                                     "This could be an indication of future failures and/or potential data loss in bad sectors.")
306
+            elif attr_name == "ssd_life_left" and int_value < 50:
307
+                failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Notice,
308
+                                                                     name,
309
+                                                                     raw_value,
310
+                                                                     "The drive has less than half of its life left.")
311
+            else:
312
+                # execute a generic check for value < threshold
313
+                if int_value and int_thresh:
314
+                    if int_value < int_thresh:
315
+                        failed_attributes[(attrid, name)] = AttributeWarning(
316
+                                                                    AttributeWarning.Warning if attr_type == 'Pre-fail' else AttributeWarning.Notice,
317
+                                                                    name,
318
+                                                                    raw_value,
319
+                                                                    "Attribute value dropped below threshold of %s" % int_thresh)
320
+
321
+
322
+        logging.debug("Failed generic attributes: %s" % (failed_attributes, ))
323
+        return failed_attributes
324
+
325
+    def check_device_attributes(self):
326
+        device_model = self.device_model
327
+        device_db_attributes = self.get_attributes_from_database(device_model)
328
+
329
+        threshold_from = re.compile('^(\d+):$')
330
+        threshold_to = re.compile('^:(\d+)$')
331
+        threshold_from_to = re.compile('^(\d+):(\d+)$')
332
+
333
+        failed_attributes = {}
334
+
335
+        for attrid, name, flag, value, worst, tresh, type, updated, when_failed, raw_value in self.smart_data['attributes']:
336
+            attrid = int(attrid)
337
+            if attrid in device_db_attributes:
338
+                db_attrs = device_db_attributes[attrid]
339
+
340
+                if isinstance(db_attrs, list):
341
+                    value_field, min_value, max_value = tuple(device_db_attributes[int(attrid)])
342
+
343
+                    check_value = value if value_field == "VALUE" else raw_value
344
+                    check_value = int(check_value or -1)
345
+
346
+                    if not (int(min_value) <= check_value <= int(max_value)):
347
+                        logging.info("Attribute %s (%s) failed: not %s <= %s <= %s" % (attrid, name, min_value, check_value, max_value))
348
+                        failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Critical, name, check_value)
349
+                elif isinstance(db_attrs, dict):
350
+                    value_field = db_attrs.get('field', 'RAW_VALUE')
351
+                    check_value = value if value_field == "VALUE" else raw_value
352
+                    check_value = int(check_value or -1)
353
+
354
+                    min_value = db_attrs.get('min', None)
355
+                    max_value = db_attrs.get('max', None)
356
+
357
+                    if min_value is None and max_value is None:
358
+                        for failure_type, threshold_key in [('WARNING', 'warn_threshold'), ('CRITICAL', 'crit_threshold')]:
359
+                            if threshold_key not in db_attrs:
360
+                                continue
361
+                            v = db_attrs.get(threshold_key)
362
+
363
+                            from_m = threshold_from.match(v)
364
+                            to_m = threshold_to.match(v)
365
+                            from_to_m = threshold_from_to.match(v)
366
+
367
+                            if (from_m and check_value >= int(from_m.group(1))) or \
368
+                                (to_m and check_value <= int(to_m.group(1))) or \
369
+                                (from_to_m and (int(from_to_m.group(1)) <= check_value <= int(from_to_m.group(2)))):
370
+
371
+                                logging.info("Attribute %s (%s) failed with %s: not within treshold %s" % (attrid, name, failure_type, v))
372
+                                failed_attributes[(attrid, name)] = AttributeWarning(failure_type, name, check_value)
373
+                    else:
374
+                        if (min_value is not None and check_value >= int(min_value)) or \
375
+                            (max_value is not None and check_value <= int(max_value)):
376
+                            logging.info("Attribute %s (%s) failed: not %s >= %s <= %s" % (attrid, name, min_value, check_value, max_value))
377
+                            failed_attributes[(attrid, name)] = AttributeWarning(AttributeWarning.Critical, name, check_value)
378
+
379
+                else:
380
+                    raise ValueError("Unknown attribute specification: %s" % db_attrs)
381
+
382
+        return failed_attributes

+ 35
- 35
src/smartcheck/convert.py View File

@@ -19,38 +19,38 @@ d = json.loads(s)
19 19
 #pprint.pprint(d)
20 20
 
21 21
 for group_name, device in sorted(d['Devices'].items(), key=lambda x: x[1]['Device'][0]):
22
-	print("")
23
-	if len(device['Device']) > 1:
24
-		print("- model:")
25
-		print('\n'.join('  - "%s"' % x for x in device['Device']))
26
-	else:
27
-		print('- model: "%s"' % device['Device'][0])
28
-
29
-	attribs = device['ID#']
30
-	if attribs:
31
-		thresholds = device['Threshs']
32
-
33
-		print("  attributes:")
34
-		for attribute_id, (field, description) in sorted(attribs.items(), key=lambda x: int(x[0])):
35
-			attrib_thresholds = thresholds.get(str(attribute_id), None)
36
-			if attrib_thresholds:
37
-				thresh1, thresh2 = tuple(attrib_thresholds)
38
-				if ':' in thresh1 and ':' in thresh2:
39
-
40
-					def fix_threshold(s):
41
-						if s.endswith(':'):
42
-							return ":%s" % s[:-1]
43
-						elif s.startswith(":"):
44
-							return "%s:" % s[1:]
45
-						else:
46
-							return s
47
-
48
-					print("    %s: # %s" % (int(attribute_id), description))
49
-					if field != 'RAW_VALUE':
50
-						print('      field: "%s"' % field)
51
-					print('      warn_threshold: "%s"' % fix_threshold(thresh1))
52
-					print('      crit_threshold: "%s"' % fix_threshold(thresh2))
53
-				else:
54
-					if 'WDC WD2000FYYZ-01UL1B0' in device['Device']:
55
-						thresh1 = 0
56
-					print('    %s: ["%s", %s, %s] # %s' % (int(attribute_id), field, thresh1, thresh2, description))
22
+    print("")
23
+    if len(device['Device']) > 1:
24
+        print("- model:")
25
+        print('\n'.join('  - "%s"' % x for x in device['Device']))
26
+    else:
27
+        print('- model: "%s"' % device['Device'][0])
28
+
29
+    attribs = device['ID#']
30
+    if attribs:
31
+        thresholds = device['Threshs']
32
+
33
+        print("  attributes:")
34
+        for attribute_id, (field, description) in sorted(attribs.items(), key=lambda x: int(x[0])):
35
+            attrib_thresholds = thresholds.get(str(attribute_id), None)
36
+            if attrib_thresholds:
37
+                thresh1, thresh2 = tuple(attrib_thresholds)
38
+                if ':' in thresh1 and ':' in thresh2:
39
+
40
+                    def fix_threshold(s):
41
+                        if s.endswith(':'):
42
+                            return ":%s" % s[:-1]
43
+                        elif s.startswith(":"):
44
+                            return "%s:" % s[1:]
45
+                        else:
46
+                            return s
47
+
48
+                    print("    %s: # %s" % (int(attribute_id), description))
49
+                    if field != 'RAW_VALUE':
50
+                        print('      field: "%s"' % field)
51
+                    print('      warn_threshold: "%s"' % fix_threshold(thresh1))
52
+                    print('      crit_threshold: "%s"' % fix_threshold(thresh2))
53
+                else:
54
+                    if 'WDC WD2000FYYZ-01UL1B0' in device['Device']:
55
+                        thresh1 = 0
56
+                    print('    %s: ["%s", %s, %s] # %s' % (int(attribute_id), field, thresh1, thresh2, description))

+ 77
- 77
src/smartcheck/main.py View File

@@ -10,101 +10,101 @@ from smartcheck.check import SMARTCheck, AttributeWarning, DEFAULT_DISKS_FILE
10 10
 
11 11
 
12 12
 def execute_smartctl(drive, interface=None, sudo=None, smartctl_path=None, smartctl_args=''):
13
-	command_line = "%s %s %s %s -a %s" % (
14
-		"sudo" if sudo else '',
15
-		smartctl_path,
16
-		'-d %s' % interface if interface else '',
17
-		smartctl_args,
18
-		drive
19
-	)
20
-	logging.debug("Executing smartctl command: %s" % command_line)
21
-
22
-	cmd = shlex.split(command_line)
23
-	process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env={'LC_ALL': 'C'})
24
-	output = process.communicate()[0]
25
-	# TODO: See if we can get hints from the smartctl exit codes:
26
-	# https://www.freebsd.org/cgi/man.cgi?query=smartctl&manpath=FreeBSD+9.0-RELEASE+and+Ports&format=html#RETURN_VALUES
27
-	# at least we should not handle if the drive is in low-power mode (spindown)
28
-#	if process.returncode:
29
-#		raise Exception("smartctl failed with status code %s" % process.returncode)
30
-	return output
13
+    command_line = "%s %s %s %s -a %s" % (
14
+        "sudo" if sudo else '',
15
+        smartctl_path,
16
+        '-d %s' % interface if interface else '',
17
+        smartctl_args,
18
+        drive
19
+    )
20
+    logging.debug("Executing smartctl command: %s" % command_line)
21
+
22
+    cmd = shlex.split(command_line)
23
+    process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env={'LC_ALL': 'C'})
24
+    output = process.communicate()[0]
25
+    # TODO: See if we can get hints from the smartctl exit codes:
26
+    # https://www.freebsd.org/cgi/man.cgi?query=smartctl&manpath=FreeBSD+9.0-RELEASE+and+Ports&format=html#RETURN_VALUES
27
+    # at least we should not handle if the drive is in low-power mode (spindown)
28
+#    if process.returncode:
29
+#        raise Exception("smartctl failed with status code %s" % process.returncode)
30
+    return output
31 31
 
32 32
 if __name__ == "__main__":
33
-	parser = ArgumentParser()
33
+    parser = ArgumentParser()
34 34
 
35
-	parser.add_argument('--disks-file', default=DEFAULT_DISKS_FILE)
35
+    parser.add_argument('--disks-file', default=DEFAULT_DISKS_FILE)
36 36
 
37
-	parser.add_argument('-s', '--sudo', help="Use sudo to execute smartctl", action='store_true', default=False)
37
+    parser.add_argument('-s', '--sudo', help="Use sudo to execute smartctl", action='store_true', default=False)
38 38
 
39
-	parser.add_argument('--smartctl-path', default="/usr/sbin/smartctl", help='Path to smartctl (default: %(default)s)')
40
-	parser.add_argument('-a', '--smartctl-args', default='-n standby', help="Other arguments passed to smartctl (default: %(default)s)")
39
+    parser.add_argument('--smartctl-path', default="/usr/sbin/smartctl", help='Path to smartctl (default: %(default)s)')
40
+    parser.add_argument('-a', '--smartctl-args', default='-n standby', help="Other arguments passed to smartctl (default: %(default)s)")
41 41
 
42
-	parser.add_argument('-i', '--interface', help="The smartctl interface specification (passed to smartctl's -d parameter")
43
-	parser.add_argument('drive', type=str, nargs='?', help="The device as passed to smartctl's positional argument")
44
-	parser.add_argument('-f', '--file', help="Use S.M.A.R.T. report from file instead of calling smartctl (Use - to read from stdin)")
42
+    parser.add_argument('-i', '--interface', help="The smartctl interface specification (passed to smartctl's -d parameter")
43
+    parser.add_argument('drive', type=str, nargs='?', help="The device as passed to smartctl's positional argument")
44
+    parser.add_argument('-f', '--file', help="Use S.M.A.R.T. report from file instead of calling smartctl (Use - to read from stdin)")
45 45
 
46
-	parser.add_argument('-x', '--exclude-notices', help='Do not report NOTICE warnings (default: %(default)s)', action='store_true', default=False)
47
-	parser.add_argument('--ignore-attributes', help='Ignore this S.M.A.R.T. attributes (id or name)', nargs='*')
48
-	parser.add_argument('-v', '--verbose', help='Verbose messages', action='store_true', default=False)
49
-	parser.add_argument('--debug', help="Print debug messages", action="store_true", default=False)
46
+    parser.add_argument('-x', '--exclude-notices', help='Do not report NOTICE warnings (default: %(default)s)', action='store_true', default=False)
47
+    parser.add_argument('--ignore-attributes', help='Ignore this S.M.A.R.T. attributes (id or name)', nargs='*')
48
+    parser.add_argument('-v', '--verbose', help='Verbose messages', action='store_true', default=False)
49
+    parser.add_argument('--debug', help="Print debug messages", action="store_true", default=False)
50 50
 
51
-	args = parser.parse_args()
51
+    args = parser.parse_args()
52 52
 
53
-	if args.file and any([args.interface, args.drive]):
54
-		parser.error('-f/--file cannot be used with a device and/or -i/--interface')
53
+    if args.file and any([args.interface, args.drive]):
54
+        parser.error('-f/--file cannot be used with a device and/or -i/--interface')
55 55
 
56
-	if args.debug:
57
-		logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s')
56
+    if args.debug:
57
+        logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s')
58 58
 
59
-	exit_code = 0
60
-	msg = ""
61
-	try:
62
-		stream = None
63
-		if args.file:
64
-			if args.file == '-':
65
-				stream = sys.stdin
66
-			else:
67
-				stream = open(args.file, 'r')
68
-		else:
69
-			stream = execute_smartctl(args.drive, args.interface, args.sudo, args.smartctl_path, args.smartctl_args)
59
+    exit_code = 0
60
+    msg = ""
61
+    try:
62
+        stream = None
63
+        if args.file:
64
+            if args.file == '-':
65
+                stream = sys.stdin
66
+            else:
67
+                stream = open(args.file, 'r')
68
+        else:
69
+            stream = execute_smartctl(args.drive, args.interface, args.sudo, args.smartctl_path, args.smartctl_args)
70 70
 
71
-		check = SMARTCheck(stream, args.disks_file)
71
+        check = SMARTCheck(stream, args.disks_file)
72 72
 
73
-		if check.data_parsed:
74
-			attribute_errors = check.check_attributes()
73
+        if check.data_parsed:
74
+            attribute_errors = check.check_attributes()
75 75
 
76
-			if args.exclude_notices:
77
-				for k in [x for x, y in attribute_errors.items() if y.level == AttributeWarning.Notice]:
78
-					del attribute_errors[k]
76
+            if args.exclude_notices:
77
+                for k in [x for x, y in attribute_errors.items() if y.level == AttributeWarning.Notice]:
78
+                    del attribute_errors[k]
79 79
 
80
-			if attribute_errors:
81
-				msg = ', '.join([ae.long_message if args.verbose else ae.short_message for ae in attribute_errors.values()])
80
+            if attribute_errors:
81
+                msg = ', '.join([ae.long_message if args.verbose else ae.short_message for ae in attribute_errors.values()])
82 82
 
83
-				if any((ae.level == AttributeWarning.Warning for ae in attribute_errors.values())):
84
-					exit_code = 1
85
-				if any((ae.level == AttributeWarning.Critical for ae in attribute_errors.values())):
86
-					exit_code = 2
83
+                if any((ae.level == AttributeWarning.Warning for ae in attribute_errors.values())):
84
+                    exit_code = 1
85
+                if any((ae.level == AttributeWarning.Critical for ae in attribute_errors.values())):
86
+                    exit_code = 2
87 87
 
88
-			if not check.check_tests():
89
-				msg = (msg.strip() + '; S.M.A.R.T. self test reported an error').lstrip(';').strip()
90
-				exit_code = 2
88
+            if not check.check_tests():
89
+                msg = (msg.strip() + '; S.M.A.R.T. self test reported an error').lstrip(';').strip()
90
+                exit_code = 2
91 91
 
92
-			if check.ata_error_count:
93
-				msg = (msg.strip() + '; %s ATA errors found' % check.ata_error_count).lstrip(';').strip()
94
-				exit_code = 2
92
+            if check.ata_error_count:
93
+                msg = (msg.strip() + '; %s ATA errors found' % check.ata_error_count).lstrip(';').strip()
94
+                exit_code = 2
95 95
 
96
-			if not exit_code:
97
-				msg = "S.M.A.R.T. data OK"
96
+            if not exit_code:
97
+                msg = "S.M.A.R.T. data OK"
98 98
 
99
-			msg = "%s: %s" % (check.device_model, msg)
100
-		else:
101
-			msg = "Could not read S.M.A.R.T. data (executed as root?)"
102
-			exit_code = 3
103
-	except Exception as ex:
104
-		msg = "Plugin failed: %s" % ex
105
-		if args.debug:
106
-			logging.exception("Plugin failed")
107
-		exit_code = 3
99
+            msg = "%s: %s" % (check.device_model, msg)
100
+        else:
101
+            msg = "Could not read S.M.A.R.T. data (executed as root?)"
102
+            exit_code = 3
103
+    except Exception as ex:
104
+        msg = "Plugin failed: %s" % ex
105
+        if args.debug:
106
+            logging.exception("Plugin failed")
107
+        exit_code = 3
108 108
 
109
-	print(msg)
110
-	sys.exit(exit_code)
109
+    print(msg)
110
+    sys.exit(exit_code)

+ 141
- 141
tests/check.py View File

@@ -8,88 +8,88 @@ db_path = os.path.join(samples_path, '../../src/smartcheck/disks.yaml')
8 8
 
9 9
 class CheckTest(unittest.TestCase):
10 10
 
11
-	def test_check_broken1(self):
12
-		with open(os.path.join(samples_path, 'seagate-barracuda-broken1.txt')) as f:
13
-			check = SMARTCheck(f)
14
-			self.assertFalse(check.check_tests())
15
-			self.assertEqual(check.ata_error_count, 8)
16
-			self.assertFalse(check.check())
17
-
18
-	def test_check_broken2(self):
19
-		with open(os.path.join(samples_path, 'seagate-barracuda-broken2.txt')) as f:
20
-			check = SMARTCheck(f)
21
-			self.assertFalse(check.check_tests())
22
-			self.assertEqual(check.ata_error_count, 52)
23
-			self.assertFalse(check.check())
24
-
25
-	def test_check_broken3(self):
26
-		with open(os.path.join(samples_path, 'WDC-WD1000FYPS-01ZKB0.txt')) as f:
27
-			check = SMARTCheck(f)
28
-			self.assertTrue(check.check_tests())  # no test ran
29
-			self.assertEqual(check.ata_error_count, 32)
30
-			self.assertFalse(check.check())
31
-
32
-	def test_smart_attributes_not_found(self):
33
-		with open(os.path.join(samples_path, 'ST2000NM0033-9ZM175.txt')) as f:
34
-			check = SMARTCheck(f, db_path)
35
-			self.assertTrue(check.check_tests())
36
-			self.assertDictEqual(check.check_attributes(), {})  # Attributes not found in disks.json
37
-			self.assertEqual(check.ata_error_count, 0)
38
-			self.assertTrue(check.check())
39
-
40
-	def test_smart_attributes_nothing_wrong(self):
41
-		with open(os.path.join(samples_path, 'WDC-WD2000FYYZ-01UL1B1.txt')) as f:
42
-			check = SMARTCheck(f, db_path)
43
-			self.assertTrue(check.check_tests())
44
-			self.assertDictEqual(check.check_attributes(), {})
45
-			self.assertEqual(check.ata_error_count, 0)
46
-			self.assertTrue(check.check())
47
-
48
-	def test_smart_attributes_min_max(self):
49
-		# from list
50
-		with open(os.path.join(samples_path, 'ST2000NM0033-9ZM175.txt')) as f:
51
-			check = SMARTCheck(f, os.path.join(samples_path, 'disks-min-max.yaml'))
52
-			self.assertTrue(check.check_tests())
53
-			self.assertDictEqual(check.check_attributes(), {
54
-				(9, 'Power_On_Hours'): AttributeWarning(AttributeWarning.Critical, 'Power_On_Hours', 16998)
55
-			})
56
-			self.assertEqual(check.ata_error_count, 0)
57
-			self.assertFalse(check.check())
58
-
59
-		# from dict
60
-		with open(os.path.join(samples_path, 'ST2000NM0033-9ZM175.txt')) as f:
61
-			check = SMARTCheck(f, os.path.join(samples_path, 'disks-min-or-max.yaml'))
62
-			self.assertTrue(check.check_tests())
63
-			self.assertDictEqual(check.check_attributes(), {
64
-				(9, 'Power_On_Hours'): AttributeWarning(AttributeWarning.Critical, 'Power_On_Hours', 16998),
65
-				(194, 'Temperature_Celsius'): AttributeWarning(AttributeWarning.Critical, 'Temperature_Celsius', 30)
66
-			})
67
-			self.assertEqual(check.ata_error_count, 0)
68
-			self.assertFalse(check.check())
69
-
70
-	def test_smart_attributes_thresholds_min(self):
71
-		for sample_file, expected_attributes in [
72
-			# only warning
73
-			('disks-thresholds.yaml', {
74
-				(9, 'Power_On_Hours'): AttributeWarning(AttributeWarning.Warning, 'Power_On_Hours', 15360)
75
-			}),
76
-			# warning and critical - critical wins
77
-			('disks-thresholds-warn-and-crit.yaml', {
78
-				(9, 'Power_On_Hours'): AttributeWarning(AttributeWarning.Critical, 'Power_On_Hours', 15360)
79
-			}),
80
-			# warning threshold with range
81
-			('disks-thresholds-range.yaml', {
82
-				(4, 'Start_Stop_Count'): AttributeWarning(AttributeWarning.Warning, 'Start_Stop_Count', 2)
83
-			})
84
-		]:
85
-			with open(os.path.join(samples_path, 'WDC-WD2000FYYZ-01UL1B1.txt')) as f:
86
-				check = SMARTCheck(f, os.path.join(samples_path, sample_file))
87
-				self.assertTrue(check.check_tests())
88
-				self.assertDictEqual(check.check_attributes(), expected_attributes)
89
-				self.assertFalse(check.check())
90
-
91
-	def test_generic_attributes(self):
92
-		base = """=== START OF INFORMATION SECTION ===
11
+    def test_check_broken1(self):
12
+        with open(os.path.join(samples_path, 'seagate-barracuda-broken1.txt')) as f:
13
+            check = SMARTCheck(f)
14
+            self.assertFalse(check.check_tests())
15
+            self.assertEqual(check.ata_error_count, 8)
16
+            self.assertFalse(check.check())
17
+
18
+    def test_check_broken2(self):
19
+        with open(os.path.join(samples_path, 'seagate-barracuda-broken2.txt')) as f:
20
+            check = SMARTCheck(f)
21
+            self.assertFalse(check.check_tests())
22
+            self.assertEqual(check.ata_error_count, 52)
23
+            self.assertFalse(check.check())
24
+
25
+    def test_check_broken3(self):
26
+        with open(os.path.join(samples_path, 'WDC-WD1000FYPS-01ZKB0.txt')) as f:
27
+            check = SMARTCheck(f)
28
+            self.assertTrue(check.check_tests())  # no test ran
29
+            self.assertEqual(check.ata_error_count, 32)
30
+            self.assertFalse(check.check())
31
+
32
+    def test_smart_attributes_not_found(self):
33
+        with open(os.path.join(samples_path, 'ST2000NM0033-9ZM175.txt')) as f:
34
+            check = SMARTCheck(f, db_path)
35
+            self.assertTrue(check.check_tests())
36
+            self.assertDictEqual(check.check_attributes(), {})  # Attributes not found in disks.json
37
+            self.assertEqual(check.ata_error_count, 0)
38
+            self.assertTrue(check.check())
39
+
40
+    def test_smart_attributes_nothing_wrong(self):
41
+        with open(os.path.join(samples_path, 'WDC-WD2000FYYZ-01UL1B1.txt')) as f:
42
+            check = SMARTCheck(f, db_path)
43
+            self.assertTrue(check.check_tests())
44
+            self.assertDictEqual(check.check_attributes(), {})
45
+            self.assertEqual(check.ata_error_count, 0)
46
+            self.assertTrue(check.check())
47
+
48
+    def test_smart_attributes_min_max(self):
49
+        # from list
50
+        with open(os.path.join(samples_path, 'ST2000NM0033-9ZM175.txt')) as f:
51
+            check = SMARTCheck(f, os.path.join(samples_path, 'disks-min-max.yaml'))
52
+            self.assertTrue(check.check_tests())
53
+            self.assertDictEqual(check.check_attributes(), {
54
+                (9, 'Power_On_Hours'): AttributeWarning(AttributeWarning.Critical, 'Power_On_Hours', 16998)
55
+            })
56
+            self.assertEqual(check.ata_error_count, 0)
57
+            self.assertFalse(check.check())
58
+
59
+        # from dict
60
+        with open(os.path.join(samples_path, 'ST2000NM0033-9ZM175.txt')) as f:
61
+            check = SMARTCheck(f, os.path.join(samples_path, 'disks-min-or-max.yaml'))
62
+            self.assertTrue(check.check_tests())
63
+            self.assertDictEqual(check.check_attributes(), {
64
+                (9, 'Power_On_Hours'): AttributeWarning(AttributeWarning.Critical, 'Power_On_Hours', 16998),
65
+                (194, 'Temperature_Celsius'): AttributeWarning(AttributeWarning.Critical, 'Temperature_Celsius', 30)
66
+            })
67
+            self.assertEqual(check.ata_error_count, 0)
68
+            self.assertFalse(check.check())
69
+
70
+    def test_smart_attributes_thresholds_min(self):
71
+        for sample_file, expected_attributes in [
72
+            # only warning
73
+            ('disks-thresholds.yaml', {
74
+                (9, 'Power_On_Hours'): AttributeWarning(AttributeWarning.Warning, 'Power_On_Hours', 15360)
75
+            }),
76
+            # warning and critical - critical wins
77
+            ('disks-thresholds-warn-and-crit.yaml', {
78
+                (9, 'Power_On_Hours'): AttributeWarning(AttributeWarning.Critical, 'Power_On_Hours', 15360)
79
+            }),
80
+            # warning threshold with range
81
+            ('disks-thresholds-range.yaml', {
82
+                (4, 'Start_Stop_Count'): AttributeWarning(AttributeWarning.Warning, 'Start_Stop_Count', 2)
83
+            })
84
+        ]:
85
+            with open(os.path.join(samples_path, 'WDC-WD2000FYYZ-01UL1B1.txt')) as f:
86
+                check = SMARTCheck(f, os.path.join(samples_path, sample_file))
87
+                self.assertTrue(check.check_tests())
88
+                self.assertDictEqual(check.check_attributes(), expected_attributes)
89
+                self.assertFalse(check.check())
90
+
91
+    def test_generic_attributes(self):
92
+        base = """=== START OF INFORMATION SECTION ===
93 93
 Model Family:     Seagate Barracuda 7200.14 (AF)
94 94
 Device Model:     ST3000DM001-1CH167
95 95
 Serial Number:    Z1F220RJ
@@ -99,31 +99,31 @@ SMART Attributes Data Structure revision number: 1
99 99
 Vendor Specific SMART Attributes with Thresholds:
100 100
 ID# ATTRIBUTE_NAME          FLAG     VALUE WORST THRESH TYPE      UPDATED  WHEN_FAILED RAW_VALUE
101 101
 """
102
-		for attr_id, attr_name in [
103
-			('5', "Reallocated_Sector_Ct"),
104
-			('197', "current_pending_sector"),
105
-			('197', "current_pending_sector_count"),
106
-			('197', "total_pending_sectors"),
107
-			('0', 'spin_up_retry_count'),
108
-			('0', 'soft_read_error_rate'),
109
-			('0', 'Reallocation_Event_Count'),
110
-			('0', 'ssd_life_left'),
111
-		]:
112
-			s = base + "%s %s 0x000f   001   000   000    Pre-fail  Always       -       1" % (
113
-				attr_id.rjust(3), attr_name.ljust(23)
114
-			)
115
-			check = SMARTCheck(s, db_path)
116
-			failed_attributes = check.check_generic_attributes()
117
-			assert len(failed_attributes) == 1
118
-			(failed_id, failed_name), warning = list(failed_attributes.items())[0]
119
-			assert int(failed_id) == int(attr_id)
120
-			assert warning.level == AttributeWarning.Notice
121
-			assert warning.value == '1'
122
-			self.assertTrue(check.check_tests())
123
-			self.assertFalse(check.check())
124
-
125
-	def test_generic_temperature_attribute(self):
126
-		base = """=== START OF INFORMATION SECTION ===
102
+        for attr_id, attr_name in [
103
+            ('5', "Reallocated_Sector_Ct"),
104
+            ('197', "current_pending_sector"),
105
+            ('197', "current_pending_sector_count"),
106
+            ('197', "total_pending_sectors"),
107
+            ('0', 'spin_up_retry_count'),
108
+            ('0', 'soft_read_error_rate'),
109
+            ('0', 'Reallocation_Event_Count'),
110
+            ('0', 'ssd_life_left'),
111
+        ]:
112
+            s = base + "%s %s 0x000f   001   000   000    Pre-fail  Always       -       1" % (
113
+                attr_id.rjust(3), attr_name.ljust(23)
114
+            )
115
+            check = SMARTCheck(s, db_path)
116
+            failed_attributes = check.check_generic_attributes()
117
+            assert len(failed_attributes) == 1
118
+            (failed_id, failed_name), warning = list(failed_attributes.items())[0]
119
+            assert int(failed_id) == int(attr_id)
120
+            assert warning.level == AttributeWarning.Notice
121
+            assert warning.value == '1'
122
+            self.assertTrue(check.check_tests())
123
+            self.assertFalse(check.check())
124
+
125
+    def test_generic_temperature_attribute(self):
126
+        base = """=== START OF INFORMATION SECTION ===
127 127
 Model Family:     Seagate Barracuda 7200.14 (AF)
128 128
 Device Model:     ST3000DM001-1CH167
129 129
 Serial Number:    Z1F220RJ
@@ -133,37 +133,37 @@ SMART Attributes Data Structure revision number: 1
133 133
 Vendor Specific SMART Attributes with Thresholds:
134 134
 ID# ATTRIBUTE_NAME          FLAG     VALUE WORST THRESH TYPE      UPDATED  WHEN_FAILED RAW_VALUE
135 135
 """
136
-		for attr_id, attr_name, attr_value, notice_expected in [
137
-			('0', 'temperature_celsius', 1, False), # too low
138
-			('0', 'temperature_celsius', 51, True), # within range
139
-			('0', 'temperature_celsius', 130, False), # out of range
140
-			('0', 'temperature_celsius_x10', 1, False), # too low
141
-			('0', 'temperature_celsius_x10', 501, True), # within range
142
-		]:
143
-			s = base + "%s %s 0x000f   %s   000   000    Pre-fail  Always       -       %s" % (
144
-				attr_id.rjust(3), attr_name.ljust(23), str(attr_value).zfill(3), attr_value
145
-			)
146
-			check = SMARTCheck(s, db_path)
147
-			failed_attributes = check.check_generic_attributes()
148
-			if notice_expected:
149
-				assert len(failed_attributes) == 1
150
-				(failed_id, failed_name), warning = list(failed_attributes.items())[0]
151
-				assert int(failed_id) == int(attr_id)
152
-				assert warning.level == AttributeWarning.Notice
153
-				assert warning.value == attr_value
154
-				self.assertFalse(check.check())
155
-			else:
156
-				assert len(failed_attributes) == 0
157
-			self.assertTrue(check.check_tests())
158
-
159
-	def test_ignore_attributes(self):
160
-		with open(os.path.join(samples_path, 'seagate-barracuda-broken1.txt')) as f:
161
-			check = SMARTCheck(f)
162
-
163
-			for ignore_value in (198, '198', 'Offline_Uncorrectable'):
164
-				failed_attributes = check.check_attributes(ignore_attributes=[ignore_value])
165
-				assert len(failed_attributes) == 1
166
-				(failed_id, failed_name), failed_attribute = list(failed_attributes.items())[0]
167
-				assert failed_id == 197
168
-				assert failed_name == "Current_Pending_Sector"
169
-				assert int(failed_attribute.value) == 24
136
+        for attr_id, attr_name, attr_value, notice_expected in [
137
+            ('0', 'temperature_celsius', 1, False), # too low
138
+            ('0', 'temperature_celsius', 51, True), # within range
139
+            ('0', 'temperature_celsius', 130, False), # out of range
140
+            ('0', 'temperature_celsius_x10', 1, False), # too low
141
+            ('0', 'temperature_celsius_x10', 501, True), # within range
142
+        ]:
143
+            s = base + "%s %s 0x000f   %s   000   000    Pre-fail  Always       -       %s" % (
144
+                attr_id.rjust(3), attr_name.ljust(23), str(attr_value).zfill(3), attr_value
145
+            )
146
+            check = SMARTCheck(s, db_path)
147
+            failed_attributes = check.check_generic_attributes()
148
+            if notice_expected:
149
+                assert len(failed_attributes) == 1
150
+                (failed_id, failed_name), warning = list(failed_attributes.items())[0]
151
+                assert int(failed_id) == int(attr_id)
152
+                assert warning.level == AttributeWarning.Notice
153
+                assert warning.value == attr_value
154
+                self.assertFalse(check.check())
155
+            else:
156
+                assert len(failed_attributes) == 0
157
+            self.assertTrue(check.check_tests())
158
+
159
+    def test_ignore_attributes(self):
160
+        with open(os.path.join(samples_path, 'seagate-barracuda-broken1.txt')) as f:
161
+            check = SMARTCheck(f)
162
+
163
+            for ignore_value in (198, '198', 'Offline_Uncorrectable'):
164
+                failed_attributes = check.check_attributes(ignore_attributes=[ignore_value])
165
+                assert len(failed_attributes) == 1
166
+                (failed_id, failed_name), failed_attribute = list(failed_attributes.items())[0]
167
+                assert failed_id == 197
168
+                assert failed_name == "Current_Pending_Sector"
169
+                assert int(failed_attribute.value) == 24

+ 85
- 85
tests/parsing.py View File

@@ -1,8 +1,8 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 try:
3
-	from StringIO import StringIO
3
+    from StringIO import StringIO
4 4
 except ImportError:
5
-	from io import StringIO
5
+    from io import StringIO
6 6
 import os
7 7
 import unittest
8 8
 from smartcheck.check import SMARTCheck
@@ -12,98 +12,98 @@ samples_path = os.path.join(os.path.dirname(__file__), 'samples')
12 12
 
13 13
 class InformationBlockParsingTest(unittest.TestCase):
14 14
 
15
-	def test_parsing(self):
16
-		for filename, expected_data in [
17
-			('seagate-barracuda-broken1.txt', {
18
-				'ata_version': 'ATA8-ACS T13/1699-D revision 4',
19
-				'device_model': 'ST3000DM001-1CH166',
20
-				'model_family': 'Seagate Barracuda 7200.14 (AF)',
21
-				'sata_version': 'SATA 3.0, 6.0 Gb/s (current: 6.0 Gb/s)',
22
-				'serial': 'Z1F220RJ'
23
-				}),
24
-			('seagate-barracuda-broken2.txt', {
25
-				'ata_version': 'ATA8-ACS T13/1699-D revision 4',
26
-				'device_model': 'ST3000DM001-1CH166',
27
-				'model_family': 'Seagate Barracuda 7200.14 (AF)',
28
-				'sata_version': 'SATA 3.0, 6.0 Gb/s (current: 6.0 Gb/s)',
29
-				'serial': 'Z1F23HW0'
30
-				}),
31
-			('areca-WD40EFRX.txt', {
32
-				'device_model': 'WD40EFRX-68WT0N0',
33
-				'serial': 'WD-WCC4E0664813'
34
-				}),
35
-			]:
15
+    def test_parsing(self):
16
+        for filename, expected_data in [
17
+            ('seagate-barracuda-broken1.txt', {
18
+                'ata_version': 'ATA8-ACS T13/1699-D revision 4',
19
+                'device_model': 'ST3000DM001-1CH166',
20
+                'model_family': 'Seagate Barracuda 7200.14 (AF)',
21
+                'sata_version': 'SATA 3.0, 6.0 Gb/s (current: 6.0 Gb/s)',
22
+                'serial': 'Z1F220RJ'
23
+                }),
24
+            ('seagate-barracuda-broken2.txt', {
25
+                'ata_version': 'ATA8-ACS T13/1699-D revision 4',
26
+                'device_model': 'ST3000DM001-1CH166',
27
+                'model_family': 'Seagate Barracuda 7200.14 (AF)',
28
+                'sata_version': 'SATA 3.0, 6.0 Gb/s (current: 6.0 Gb/s)',
29
+                'serial': 'Z1F23HW0'
30
+                }),
31
+            ('areca-WD40EFRX.txt', {
32
+                'device_model': 'WD40EFRX-68WT0N0',
33
+                'serial': 'WD-WCC4E0664813'
34
+                }),
35
+            ]:
36 36
 
37
-			with open(os.path.join(samples_path, filename)) as f:
38
-				check = SMARTCheck(f)
39
-				self.assertDictEqual(check.information, expected_data)
37
+            with open(os.path.join(samples_path, filename)) as f:
38
+                check = SMARTCheck(f)
39
+                self.assertDictEqual(check.information, expected_data)
40 40
 
41
-	def test_information_section_missing_empty(self):
42
-		check = SMARTCheck(StringIO(""))
43
-		self.assertDictEqual(check.information, {})
41
+    def test_information_section_missing_empty(self):
42
+        check = SMARTCheck(StringIO(""))
43
+        self.assertDictEqual(check.information, {})
44 44
 
45
-	def test_information_section_missing(self):
46
-		with open(os.path.join(samples_path, 'no-information-section.txt')) as f:
47
-			check = SMARTCheck(f)
48
-			self.assertDictEqual(check.information, {})
45
+    def test_information_section_missing(self):
46
+        with open(os.path.join(samples_path, 'no-information-section.txt')) as f:
47
+            check = SMARTCheck(f)
48
+            self.assertDictEqual(check.information, {})
49 49
 
50 50
 
51 51
 class SMARTDataParsingTest(unittest.TestCase):
52
-	def test_parsing(self):
53
-		for filename, overall_health, attributes in [
54
-			('seagate-barracuda-broken1.txt', 'PASSED', [
55
-				('1', 'Raw_Read_Error_Rate', '0x000f', '103', '099', '006', 'Pre-fail', 'Always', '-', '5845168'),
56
-				('3', 'Spin_Up_Time', '0x0003', '095', '095', '000', 'Pre-fail', 'Always', '-', '0'),
57
-				('4', 'Start_Stop_Count', '0x0032', '100', '100', '020', 'Old_age', 'Always', '-', '7'),
58
-				('5', 'Reallocated_Sector_Ct', '0x0033', '100', '100', '010', 'Pre-fail', 'Always', '-', '0'),
59
-				('7', 'Seek_Error_Rate', '0x000f', '087', '055', '030', 'Pre-fail', 'Always', '-', '643382224'),
60
-				('9', 'Power_On_Hours', '0x0032', '074', '074', '000', 'Old_age', 'Always', '-', '23115'),
61
-				('10', 'Spin_Retry_Count', '0x0013', '100', '100', '097', 'Pre-fail', 'Always', '-', '0'),
62
-				('12', 'Power_Cycle_Count', '0x0032', '100', '100', '020', 'Old_age', 'Always', '-', '7'),
63
-				('183', 'Runtime_Bad_Block', '0x0032', '100', '100', '000', 'Old_age', 'Always', '-', '0'),
64
-				('184', 'End-to-End_Error', '0x0032', '100', '100', '099', 'Old_age', 'Always', '-', '0'),
65
-				('187', 'Reported_Uncorrect', '0x0032', '092', '092', '000', 'Old_age', 'Always', '-', '8'),
66
-				('188', 'Command_Timeout', '0x0032', '100', '100', '000', 'Old_age', 'Always', '-', '0 0 0'),
67
-				('189', 'High_Fly_Writes', '0x003a', '100', '100', '000', 'Old_age', 'Always', '-', '0'),
68
-				('190', 'Airflow_Temperature_Cel', '0x0022', '071', '064', '045', 'Old_age', 'Always', '-', '29 (Min/Max 24/32)'),
69
-				('191', 'G-Sense_Error_Rate', '0x0032', '100', '100', '000', 'Old_age', 'Always', '-', '0'),
70
-				('192', 'Power-Off_Retract_Count', '0x0032', '100', '100', '000', 'Old_age', 'Always', '-', '5'),
71
-				('193', 'Load_Cycle_Count', '0x0032', '100', '100', '000', 'Old_age', 'Always', '-', '1574'),
72
-				('194', 'Temperature_Celsius', '0x0022', '029', '040', '000', 'Old_age', 'Always', '-', '29 (0 22 0 0 0)'),
73
-				('197', 'Current_Pending_Sector', '0x0012', '100', '100', '000', 'Old_age', 'Always', '-', '24'),
74
-				('198', 'Offline_Uncorrectable', '0x0010', '100', '100', '000', 'Old_age', 'Offline', '-', '24'),
75
-				('199', 'UDMA_CRC_Error_Count', '0x003e', '200', '200', '000', 'Old_age', 'Always', '-', '0'),
76
-				('240', 'Head_Flying_Hours', '0x0000', '100', '253', '000', 'Old_age', 'Offline', '-', '23044h+44m+28.078s'),
77
-				('241', 'Total_LBAs_Written', '0x0000', '100', '253', '000', 'Old_age', 'Offline', '-', '161370349869'),
78
-				('242', 'Total_LBAs_Read', '0x0000', '100', '253', '000', 'Old_age', 'Offline', '-', '78560290534'),
79
-			])
80
-		]:
81
-			with open(os.path.join(samples_path, filename)) as f:
82
-				check = SMARTCheck(f)
83
-				self.assertEqual(check.smart_data['overall_health_status'], overall_health)
84
-				self.assertEqual(check.smart_data['attributes'], attributes)
52
+    def test_parsing(self):
53
+        for filename, overall_health, attributes in [
54
+            ('seagate-barracuda-broken1.txt', 'PASSED', [
55
+                ('1', 'Raw_Read_Error_Rate', '0x000f', '103', '099', '006', 'Pre-fail', 'Always', '-', '5845168'),
56
+                ('3', 'Spin_Up_Time', '0x0003', '095', '095', '000', 'Pre-fail', 'Always', '-', '0'),
57
+                ('4', 'Start_Stop_Count', '0x0032', '100', '100', '020', 'Old_age', 'Always', '-', '7'),
58
+                ('5', 'Reallocated_Sector_Ct', '0x0033', '100', '100', '010', 'Pre-fail', 'Always', '-', '0'),
59
+                ('7', 'Seek_Error_Rate', '0x000f', '087', '055', '030', 'Pre-fail', 'Always', '-', '643382224'),
60
+                ('9', 'Power_On_Hours', '0x0032', '074', '074', '000', 'Old_age', 'Always', '-', '23115'),
61
+                ('10', 'Spin_Retry_Count', '0x0013', '100', '100', '097', 'Pre-fail', 'Always', '-', '0'),
62
+                ('12', 'Power_Cycle_Count', '0x0032', '100', '100', '020', 'Old_age', 'Always', '-', '7'),
63
+                ('183', 'Runtime_Bad_Block', '0x0032', '100', '100', '000', 'Old_age', 'Always', '-', '0'),
64
+                ('184', 'End-to-End_Error', '0x0032', '100', '100', '099', 'Old_age', 'Always', '-', '0'),
65
+                ('187', 'Reported_Uncorrect', '0x0032', '092', '092', '000', 'Old_age', 'Always', '-', '8'),
66
+                ('188', 'Command_Timeout', '0x0032', '100', '100', '000', 'Old_age', 'Always', '-', '0 0 0'),
67
+                ('189', 'High_Fly_Writes', '0x003a', '100', '100', '000', 'Old_age', 'Always', '-', '0'),
68
+                ('190', 'Airflow_Temperature_Cel', '0x0022', '071', '064', '045', 'Old_age', 'Always', '-', '29 (Min/Max 24/32)'),
69
+                ('191', 'G-Sense_Error_Rate', '0x0032', '100', '100', '000', 'Old_age', 'Always', '-', '0'),
70
+                ('192', 'Power-Off_Retract_Count', '0x0032', '100', '100', '000', 'Old_age', 'Always', '-', '5'),
71
+                ('193', 'Load_Cycle_Count', '0x0032', '100', '100', '000', 'Old_age', 'Always', '-', '1574'),
72
+                ('194', 'Temperature_Celsius', '0x0022', '029', '040', '000', 'Old_age', 'Always', '-', '29 (0 22 0 0 0)'),
73
+                ('197', 'Current_Pending_Sector', '0x0012', '100', '100', '000', 'Old_age', 'Always', '-', '24'),
74
+                ('198', 'Offline_Uncorrectable', '0x0010', '100', '100', '000', 'Old_age', 'Offline', '-', '24'),
75
+                ('199', 'UDMA_CRC_Error_Count', '0x003e', '200', '200', '000', 'Old_age', 'Always', '-', '0'),
76
+                ('240', 'Head_Flying_Hours', '0x0000', '100', '253', '000', 'Old_age', 'Offline', '-', '23044h+44m+28.078s'),
77
+                ('241', 'Total_LBAs_Written', '0x0000', '100', '253', '000', 'Old_age', 'Offline', '-', '161370349869'),
78
+                ('242', 'Total_LBAs_Read', '0x0000', '100', '253', '000', 'Old_age', 'Offline', '-', '78560290534'),
79
+            ])
80
+        ]:
81
+            with open(os.path.join(samples_path, filename)) as f:
82
+                check = SMARTCheck(f)
83
+                self.assertEqual(check.smart_data['overall_health_status'], overall_health)
84
+                self.assertEqual(check.smart_data['attributes'], attributes)
85 85
 
86
-	def test_data_section_missing(self):
87
-		check = SMARTCheck(StringIO(""))
88
-		self.assertDictEqual(check.smart_data, {})
86
+    def test_data_section_missing(self):
87
+        check = SMARTCheck(StringIO(""))
88
+        self.assertDictEqual(check.smart_data, {})
89 89
 
90
-	def test_data_section_missing2(self):
91
-		with open(os.path.join(samples_path, 'no-data-section.txt')) as f:
92
-			check = SMARTCheck(f)
93
-			self.assertDictEqual(check.smart_data, {})
90
+    def test_data_section_missing2(self):
91
+        with open(os.path.join(samples_path, 'no-data-section.txt')) as f:
92
+            check = SMARTCheck(f)
93
+            self.assertDictEqual(check.smart_data, {})
94 94
 
95 95
 
96 96
 class SelfTestParsingTest(unittest.TestCase):
97 97
 
98
-	def test_parsing(self):
99
-		for filename, tests in [
100
-			('seagate-barracuda-broken1.txt', [
101
-				('1', 'Extended offline', 'Completed: read failure', '80%', '23113', '1737376544'),
102
-				('2', 'Extended offline', 'Completed: read failure', '80%', '23000', '1737376544'),
103
-				('3', 'Extended offline', 'Interrupted (host reset)', '80%', '22998', '-'),
104
-				('4', 'Extended offline', 'Completed without error', '00%', '5', '-'),
105
-			])
106
-		]:
107
-			with open(os.path.join(samples_path, filename)) as f:
108
-				check = SMARTCheck(f)
109
-				self.assertEqual(check.self_tests['test_results'], tests)
98
+    def test_parsing(self):
99
+        for filename, tests in [
100
+            ('seagate-barracuda-broken1.txt', [
101
+                ('1', 'Extended offline', 'Completed: read failure', '80%', '23113', '1737376544'),
102
+                ('2', 'Extended offline', 'Completed: read failure', '80%', '23000', '1737376544'),
103
+                ('3', 'Extended offline', 'Interrupted (host reset)', '80%', '22998', '-'),
104
+                ('4', 'Extended offline', 'Completed without error', '00%', '5', '-'),
105
+            ])
106
+        ]:
107
+            with open(os.path.join(samples_path, filename)) as f:
108
+                check = SMARTCheck(f)
109
+                self.assertEqual(check.self_tests['test_results'], tests)

Loading…
Cancel
Save