1#!@@PYTHON@@
2#
3# Wildcard-plugin to monitor S.M.A.R.T attribute values through smartctl,
4# which is part of smartmontools package:
5#         http://smartmontools.sourceforge.net/
6#
7# To monitor a S.M.A.R.T device, link smart_<device> to this file.
8# E.g.
9#    ln -s /usr/share/munin/plugins/smart_ /etc/munin/plugins/smart_hda
10# ...will monitor /dev/hda.
11#
12# Needs following minimal configuration in plugin-conf.d/munin-node:
13#   [smart_*]
14#   user root
15#   group disk
16#
17# Parameters
18#       smartpath     - Specify path to smartctl program (Default: /usr/sbin/smartctl)
19#       smartargs     - Override '-a' argument passed to smartctl with '-A -i'+smartargs
20#       ignorestandby - Ignore the standby state of the drive and perform SMART query.
21#                       Default: False
22#       ignoreexit    - Bits in smartctl exit code to ignore, e.g. 64. Default: 0
23#
24# Parameters can be specified on a per-drive basis, eg:
25#   [smart_hda]
26#   user root
27#   group disk
28#   env.smartargs -H -c -l error -l selftest -l selective -d ata
29#   env.smartpath /usr/local/sbin/smartctl
30#
31#   [smart_twa0-1]
32#   user root
33#   group disk
34#   env.smartargs -H -l error -d 3ware,1
35#   env.ignorestandby True
36#
37#   [smart_twa0-2]
38#   user root
39#   group disk
40#   env.smartargs -H -l error -d 3ware,2
41#
42# Author: Nicolas Stransky  <Nico@stransky.cx>
43#
44# v1.0 22/08/2004 - First draft
45# v1.2 28/08/2004 - Clean up the code, add a verbose option
46# v1.3 14/11/2004 - Compatibility with python<2.2. See comments in the code
47# v1.4 17/11/2004 - Deal with non zero exit codes of smartctl
48#                 - config now prints the critical thresholds, as reported by smartctl
49# v1.5 18/11/2004 - Plot smartctl_exit_code bitmask
50# v1.6 21/11/2004 - Add autoconf and suggest capabilities
51#                 - smartctl path can be passed through "smartpath" environment variable
52#                 - Additional smartctl args can be passed through "smartargs" environment variable
53# v1.7 29/11/2004 - Add suggest capabilities for NetBSD, OpenBSD, FreeBSD and SunOS.
54#                 - Allow to override completely the smartctl arguments with "smartargs"
55# v1.8 16/02/2005 - Exit status field now only triggers warnings, not criticals.
56# v1.9 07/07/2005 - Allow to query several drives on the same 3ware card.
57#                 - Correct a bug when '-i' was not listed in smartargs
58#                 - Don't fail if no value was obtained for hard drive model
59# v1.10 19/08/2005 - smartctl_exit_code is now a numerical value
60# v2.0  08/05/2009 - Correct bug in the interpretation of smartctl_exit_code
61#                  - New option to suppress SMART warnings in munin
62#                  - Temporary lack of output for previously existing drive now reports U
63#                  - The plugin now contains its own documentation for use with munindoc
64#                  - Removed python<2.2 compatibility comments
65#                  - Better autodetection of drives
66#                  - Don't spin up devices in a low-power mode.
67# v2.1 2012-02-14 - Add support for Darwin (Mac OS X).
68#                 - Print the last line of smartctl output to verbose
69#                   log to aid in understanding why smartctl exited with
70#                   a nonzero status.
71# v2.2 2014-08-22 - Add "ignoreexit" environment variable.
72# v2.3 2018-08-10 - Improve readability, reduce code duplication, avoid shell expansion.
73# v2.4 2018-12-19 - Ignore invalid threshold data
74#
75# Copyright (c) 2004-2009 Nicolas Stransky.
76# Copyright (c) 2018 Lars Kruse <devel@sumpfralle.de>
77#
78# Permission to use, copy, and modify this software with or without fee
79# is hereby granted, provided that this entire notice is included in
80# all source code copies of any software which is or includes a copy or
81# modification of this software.
82#
83# THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR
84# IMPLIED WARRANTY. IN PARTICULAR, NONE OF THE AUTHORS MAKES ANY
85# REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE
86# MERCHANTABILITY OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR
87# PURPOSE.
88#
89#
90# Magic markers
91#  #%# capabilities=autoconf suggest
92#  #%# family=auto
93
94import collections
95import os
96import sys
97import string
98import pickle
99import subprocess
100
101# Increase verbosity (True/False) -> use munin-run --pidebug
102verbose = True if os.getenv('MUNIN_DEBUG') == '1' else False
103
104# collect configuration from environment
105smartctl_bin = os.getenv('smartpath', '/usr/sbin/smartctl')
106smartctl_args = os.getenv('smartargs', '-a')
107smartctl_ignore_standby = bool(os.getenv('ignorestandby', False))
108smartctl_ignore_exitcode_bitmask = int(os.getenv('ignoreexit', 0))
109
110# You may edit the following 3 variables
111# Suppress SMART warnings (True/False)
112report_warnings = True
113# You may not modify anything below this line
114
115plugin_version = "2.4"
116
117# some disks report invalid threshold values
118INVALID_THRESHOLDS_BLACKLIST = {"---"}
119
120
121SmartCtlParseResult = collections.namedtuple("SmartCtlParseResult",
122                                             ("has_failed", "smart_data", "model", "is_empty"))
123
124
125def verboselog(s):
126    if verbose:
127        sys.stderr.write('{}: {}\n'.format(plugin_name, s))
128
129
130def guess_full_path(hard_drive):
131    """ try to find the full path for a given hard disk name
132
133    None is returned if no device node was found.
134    """
135    for dev_dir in ('/dev', '/dev/disk/by-id'):
136        full_path = os.path.join(dev_dir, hard_drive)
137        if os.path.exists(full_path):
138            return full_path
139    else:
140        return None
141
142
143def is_fatal_exitcode(exit_code):
144    # The exit code represents a bitmask.
145    # Bits 0/1/2 belong to fatal errors (see smartctl's man page). Check if one of these is set.
146    return (exit_code & 0b111) > 0
147
148
149def read_values(hard_drive):
150    smart_values = {}
151    try:
152        verboselog('Reading S.M.A.R.T values')
153        os.putenv('LC_ALL', 'C')
154        device = guess_full_path(hard_drive)
155        command_tokens = [smartctl_bin] + smartctl_args.split()
156        if not smartctl_ignore_standby:
157            command_tokens.extend(('-n', 'standby'))
158        command_tokens.extend(('-A', '-i', device))
159        proc = subprocess.Popen(command_tokens, stdout=subprocess.PIPE)
160        stdout, stderr = proc.communicate()
161        in_table_data = False
162        last_output_line = None
163        model = "unknown"
164        for line in stdout.decode().splitlines():
165            if not line:
166                # the table is finished
167                in_table_data = False
168            elif not in_table_data:
169                # process header data
170                if line.startswith('Device Model:') or line.startswith('Device:'):
171                    value = line.split(':', 1)[1].strip()
172                    # ignore the "Version" string
173                    model = ' '.join(token for token in value.split() if token != 'Version')
174                elif line.startswith('ID# ATTRIBUTE_NAME'):
175                    # Start reading the Attributes block
176                    in_table_data = True
177                else:
178                    # we can ignore other header lines
179                    pass
180            else:
181                # this is a data table row
182                tokens = line.split()
183                key = tokens[1].replace('-', '_')
184                value = tokens[3]
185                threshold = None if tokens[5] in INVALID_THRESHOLDS_BLACKLIST else tokens[5]
186                smart_values[key] = {"value": value, "threshold": threshold}
187            last_output_line = line
188        real_exit_code = proc.returncode
189        if real_exit_code > 0:
190            # Allow to turn off warnings for some bits
191            num_exit_status = real_exit_code & ~smartctl_ignore_exitcode_bitmask
192        else:
193            num_exit_status = 0
194        if num_exit_status != 0:
195            if is_fatal_exitcode(num_exit_status):
196                verboselog('smartctl cannot access S.M.A.R.T values on drive {}. Command exited '
197                           'with code {}'.format(hard_drive, num_exit_status))
198                verboselog(last_output_line)
199            else:
200                # the error is not fatal, but we should announce a warning
201                verboselog('smartctl exited with code {}. {} may be FAILING RIGHT NOW!'
202                           .format(num_exit_status, hard_drive))
203    except Exception as exc:
204        verboselog('Cannot access S.M.A.R.T values ({})! Check user rights or proper '
205                   'smartmontools installation/arguments.'.format(exc))
206        sys.exit(1)
207    if not smart_values:
208        verboselog("Can't find any S.M.A.R.T values in smartctl output!")
209        is_empty = True
210    else:
211        is_empty = False
212    smart_values["smartctl_exit_status"] = {"value": str(num_exit_status), "threshold": "1"}
213    return SmartCtlParseResult(is_fatal_exitcode(real_exit_code), smart_values, model, is_empty)
214
215
216def get_state_filename(hard_drive):
217    statefiledir = os.environ['MUNIN_PLUGSTATE']
218    return os.path.join(statefiledir, "smart-{}.state".format(hard_drive))
219
220
221def open_state_file(hard_drive, mode):
222    return open(get_state_filename(hard_drive), mode)
223
224
225def update_state_file(hard_drive, model, smart_values):
226    data_storage = dict(smart_values)
227    data_storage["model"] = model
228    try:
229        verboselog('Saving statefile')
230        pickle.dump(data_storage, open_state_file(hard_drive, "wb"))
231    except Exception as exc:
232        verboselog('Error trying to save state file ({})! Check access rights'.format(exc))
233
234
235def parse_state_file(hard_drive):
236    data = pickle.load(open_state_file(hard_drive, "rb"))
237    model = data.pop("model", "unknown")
238    return model, data
239
240
241def print_plugin_values(hard_drive, is_empty, smart_values):
242    if not is_empty:
243        verboselog('Printing S.M.A.R.T values')
244        for key, content in smart_values.items():
245            print("{}.value {}".format(key, content["value"]))
246    else:
247        print_unknown_from_statefile(hard_drive)
248
249
250def print_config(hard_drive):
251    if os.path.exists(get_state_filename(hard_drive)):
252        try:
253            verboselog('Try to recall previous S.M.A.R.T attributes for {}'
254                       .format(hard_drive))
255            model, smart_values_state = parse_state_file(hard_drive)
256        except Exception as exc:
257            verboselog('Error opening existing state file ({})!'.format(exc))
258            sys.exit(1)
259    else:
260        verboselog('No state file, reading S.M.A.R.T values for the first time')
261        parsed_data = read_values(hard_drive)
262        update_state_file(hard_drive, parsed_data.model, parsed_data.smart_data)
263        model = parsed_data.model
264        smart_values_state = parsed_data.smart_data
265
266    verboselog('Printing configuration')
267    print('graph_title S.M.A.R.T values for drive {}'.format(hard_drive))
268    print('graph_vlabel Attribute S.M.A.R.T value')
269    print('graph_args --base 1000 --lower-limit 0')
270    print('graph_category disk')
271    print('graph_info This graph shows the value of all S.M.A.R.T attributes of drive {} ({}). '
272          'smartctl_exit_status is the return value of smartctl. A non-zero return value '
273          'indicates an error, a potential error, or a fault on the drive.'
274          .format(hard_drive, model))
275    for key, content in smart_values_state.items():
276        print('{}.label {}'.format(key, key))
277        if report_warnings:
278            if content["threshold"] is None:
279                # we did not parse a valid threshold
280                pass
281            elif key == 'smartctl_exit_status':
282                level = 'warning'
283                print('{}.{} {}'.format(key, level, content["threshold"]))
284            else:
285                level = "critical"
286                print('{}.{} {}:'.format(key, level, content["threshold"]))
287
288
289def print_unknown_from_statefile(hard_drive):
290    if os.path.exists(get_state_filename(hard_drive)):
291        try:
292            verboselog('Failed to get S.M.A.R.T values from drive. Try to recall previous '
293                       'S.M.A.R.T attributes for {}'.format(hard_drive))
294            model, smart_values_state = parse_state_file(hard_drive)
295        except Exception as exc:
296            verboselog('Error opening existing state file ({})!'.format(exc))
297            sys.exit(1)
298    else:
299        verboselog('No state file, reading S.M.A.R.T values for the first time')
300        sys.exit(1)
301
302    verboselog('Printing unknown values for all attributes in state file')
303    for key in smart_values_state.keys():
304        print('{}.value U'.format(key))
305
306
307def get_hard_drive_name(plugin_name):
308    try:
309        name = plugin_name.split('_', 1)[1]
310        if os.uname()[0] == "SunOS":
311            # Special handling of hard_drive names starting with "rdsk" or "rmt".
312            # These are changed from "rdsk0" to "rdsk/0".
313            for prefix in ('rdsk', 'rmt'):
314                if name.startswith(prefix):
315                    name = os.path.join(prefix, name[len(prefix):])
316        if guess_full_path(name) is None:
317            # For 3ware cards, we have to set multiple plugins for the same hard drive name.
318            # Let's see if we find a '-' in the drive name.
319            if '-' in name:
320                name = name.split('-')[0]
321        # Chech that the drive exists in /dev
322        if guess_full_path(name) is None:
323            verboselog('/dev/(disk/by-id/)? {} not found!'.format(name))
324            sys.exit(1)
325        return name
326    except Exception as exc:
327        verboselog("No S.M.A.R.T device name found in plugin's symlink ({})!".format(exc))
328        sys.exit(1)
329
330
331def find_smart_drives():
332    # Try to autodetect Linux, *BSD, SunOS drives. Don't try to autodetect drives on a 3Ware card.
333    drives = []
334    if os.uname()[0] == "Linux":
335        if os.path.exists('/sys/block/'):
336            # Running 2.6
337            try:
338                for drive in os.listdir('/sys/block/'):
339                    if drive[:2] in ['md', 'fd', 'lo', 'ra', 'dm']:
340                        continue  # Ignore MD, Floppy, loop , RAM and LVM devices.
341                    try:
342                        verboselog('Trying {} ...'.format(drive))
343                        parsed_data = read_values(drive)
344                        if not parsed_data.has_failed and not parsed_data.is_empty:
345                            drives.append(drive)
346                    except Exception:
347                        continue
348            except Exception:
349                verboselog('Failed to list devices in /sys/block')
350        else:
351            verboselog('Not running linux2.6, failing back to /proc/partitions')
352            try:
353                partitions = open('/proc/partitions', 'r')
354                lines = partitions.readlines()
355                for line in lines:
356                    words = line.split()
357                    if len(words) == 0 or words[0][0] not in string.digits:
358                        continue
359                    if words[0] in ['1', '9', '58', '254']:
360                        # Ignore RAM, md, LVM and LVM2 devices
361                        continue
362                    if words[-1][-1] not in string.digits:
363                        try:
364                            verboselog('Trying '+words[-1]+'...')
365                            parsed_data = read_values(words[-1])
366                            if not parsed_data.has_failed and not parsed_data.is_empty:
367                                drives.append(words[-1])
368                        except Exception:
369                            continue
370                verboselog('Found drives in /proc/partitions ! '+str(drives))
371            except Exception as exc:
372                verboselog('Failed to list devices in /proc/partitions: {}'.format(exc))
373    elif os.uname()[0] == "OpenBSD":
374        try:
375            sysctl_kerndisks = os.popen('sysctl hw.disknames')
376            kerndisks = sysctl_kerndisks.readline().strip()
377            for drive in kerndisks[kerndisks.rindex('=')+1:].split(','):
378                if drive[:2] in ['md', 'cd', 'fd']:
379                    # Ignore Memory Disks, CD-ROM drives and Floppy
380                    continue
381                try:
382                    verboselog('Trying '+drive+'c...')
383                    parsed_data = read_values(drive + 'c')
384                    if not parsed_data.has_failed and not parsed_data.is_empty:
385                        drives.append(drive+'c')
386                except Exception:
387                    continue
388        except Exception as exc:
389            verboselog('Failed to list OpenBSD disks: {}'.format(exc))
390    elif os.uname()[0] == "FreeBSD":
391        try:
392            sysctl_kerndisks = os.popen('sysctl kern.disks')
393            kerndisks = sysctl_kerndisks.readline().strip()
394            for drive in kerndisks.split()[1:]:
395                if drive[:2] in ['md', 'cd', 'fd']:
396                    # Ignore Memory Disks, CD-ROM drives and Floppy
397                    continue
398                try:
399                    verboselog('Trying '+drive+'...')
400                    parsed_data = read_values(drive)
401                    if not parsed_data.has_failed and not parsed_data.is_empty:
402                        drives.append(drive)
403                except Exception:
404                    continue
405        except Exception as exc:
406            verboselog('Failed to list FreeBSD disks: {}'.format(exc))
407    elif os.uname()[0] == "Darwin":
408        try:
409            from glob import glob
410            for drivepath in glob('/dev/disk[0-9]'):
411                try:
412                    drive = os.path.basename(drivepath)
413                    verboselog('Trying '+drive+'...')
414                    parsed_data = read_values(drive)
415                    if not parsed_data.has_failed and not parsed_data.is_empty:
416                        drives.append(drive)
417                except Exception:
418                    continue
419        except Exception as exc:
420            verboselog('Failed to list Darwin disks: {}'.format(exc))
421    elif os.uname()[0] == "NetBSD":
422        try:
423            sysctl_kerndisks = os.popen('sysctl hw.disknames')
424            kerndisks = sysctl_kerndisks.readline().strip()
425            for drive in kerndisks.split()[2:]:
426                if drive[:2] in ['md', 'cd', 'fd']:
427                    # Ignore Memory Disks, CD-ROM drives and Floppy
428                    continue
429                try:
430                    verboselog('Trying {} ...'.format(drive))
431                    parsed_data = read_values(drive + 'c')
432                    if not parsed_data.has_failed and not parsed_data.is_empty:
433                        drives.append(drive + 'c')
434                except Exception:
435                    continue
436        except Exception as exc:
437            verboselog('Failed to list NetBSD disks: {}'.format(exc))
438    elif os.uname()[0] == "SunOS":
439        try:
440            from glob import glob
441            for drivepath in glob('/dev/rdsk/*s2'):
442                try:
443                    drive = os.path.basename(drivepath)
444                    verboselog('Trying rdsk {} ...'.format(drive))
445                    parsed_data = read_values('rdsk' + drive)
446                    if not parsed_data.has_failed and not parsed_data.is_empty:
447                        drives.append('rdsk' + drive)
448                except Exception:
449                    continue
450            for drivepath in glob('/dev/rmt/*'):
451                try:
452                    drive = os.path.basename(drivepath)
453                    verboselog('Trying rmt {} ...'.format(drive))
454                    parsed_data = read_values('rmt' + drive)
455                    if not parsed_data.has_failed and not parsed_data.is_empty:
456                        drives.append('rmt' + drive)
457                except Exception:
458                    continue
459        except Exception:
460            verboselog('Failed to list SunOS disks')
461    return drives
462
463
464""" Main part """
465
466plugin_name = list(os.path.split(sys.argv[0]))[1]
467verboselog("plugins' UID: {:d} / plugins' GID: {:d}".format(os.geteuid(), os.getegid()))
468
469# Parse arguments
470if len(sys.argv) > 1:
471    if sys.argv[1] == "config":
472        hard_drive = get_hard_drive_name(plugin_name)
473        print_config(hard_drive)
474        sys.exit(0)
475    elif sys.argv[1] == "autoconf":
476        if os.path.exists(smartctl_bin):
477            if not find_smart_drives():
478                print('no (no drives accessible)')
479            else:
480                print('yes')
481            sys.exit(0)
482        else:
483            print('no (smartmontools not found)')
484            sys.exit(0)
485    elif sys.argv[1] == "suggest":
486        for drive in find_smart_drives():
487            print(drive)
488        sys.exit(0)
489    elif sys.argv[1] == "version":
490        print('smart_ Munin plugin, version '+plugin_version)
491        sys.exit(0)
492    elif sys.argv[1] != "":
493        verboselog('unknown argument "'+sys.argv[1]+'"')
494        sys.exit(1)
495
496# No argument given, doing the real job:
497hard_drive = get_hard_drive_name(plugin_name)
498parsed_data = read_values(hard_drive)
499if not parsed_data.is_empty:
500    update_state_file(hard_drive, parsed_data.model, parsed_data.smart_data)
501print_plugin_values(hard_drive, parsed_data.is_empty, parsed_data.smart_data)
502sys.exit(0)
503
504
505# The following is the smart_ plugin documentation, intended to be used with munindoc
506"""
507=head1 NAME
508
509smart_ - Munin wildcard-plugin to monitor S.M.A.R.T. attribute values through smartctl
510
511=head1 APPLICABLE SYSTEMS
512
513Node with B<Python> interpreter and B<smartmontools> (http://smartmontools.sourceforge.net/)
514installed and in function.
515
516=head1 CONFIGURATION
517
518=head2 Create link in service directory
519
520To monitor a S.M.A.R.T device, create a link in the service directory
521of the munin-node named smart_<device>, which is pointing to this file.
522
523E.g.
524
525ln -s /usr/share/munin/plugins/smart_ /etc/munin/plugins/smart_hda
526
527...will monitor /dev/hda.
528
529=head2 Grant privileges in munin-node
530
531The plugin must be run under high privileged user B<root>, to get access to the raw device.
532
533So following minimal configuration in plugin-conf.d/munin-node is needed.
534
535=over 2
536
537  [smart_*]
538  user root
539  group disk
540
541=back
542
543=head2 Set Parameter if needed
544
545  smartpath     - Specify path to smartctl program (Default: /usr/sbin/smartctl)
546  smartargs     - Override '-a' argument passed to smartctl with '-A -i'+smartargs
547  ignorestandby - Ignore the standby state of the drive and perform SMART query. Default: False
548  ignoreexit    - Bits in smartctl exit code to ignore, e.g. 64. Default: 0
549
550Parameters can be specified on a per-drive basis, eg:
551
552=over 2
553
554  [smart_hda]
555  user root
556  env.smartargs -H -c -l error -l selftest -l selective -d ata
557  env.smartpath /usr/local/sbin/smartctl
558
559=back
560
561In particular, for SATA drives, with older versions of smartctl:
562
563=over 2
564
565  [smart_sda]
566  user root
567  env.smartargs -d ata -a
568
569  [smart_twa0-1]
570  user root
571  env.smartargs -H -l error -d 3ware,1
572  env.ignorestandby True
573
574  [smart_twa0-2]
575  user root
576  env.smartargs -H -l error -d 3ware,2
577
578=back
579
580The C<ignoreexit> parameter can be useful to exclude some bits in smartctl exit
581code, which is a bit mask described in its main page, from consideration. For
582example, if the drive had any errors in the past, the exit code would always
583have its bit 6 ("The device error log contains records of errors.") set, even if
584the errors happened a long time ago and are not relevant any more. To avoid
585getting munin warnings about this you can use
586
587=over 2
588
589  [smart_sda]
590  env.ignoreexit 64
591
592=back
593
594
595=head1 INTERPRETATION
596
597If a device supports the B<Self-Monitoring, Analysis
598and Reporting Technology (S.M.A.R.T.)> it offers readable
599access to the attribute table. There you find the B<raw value>,
600a B<normalised value> and a B<threshold> (set by the vendor)
601for each attribute, that is supported by that device.
602
603The meaning and handling of the raw value is a secret of the
604vendors embedded S.M.A.R.T.-Software on the disk. The only
605relevant info from our external view is the B<normalised value>
606in comparison with the B<threshold>. If the attributes value is
607equal or below the threshold, it signals its failure and
608the B<health status> of the device will switch from B<passed> to B<failed>.
609
610This plugin fetches the B<normalised values of all SMART-Attributes>
611and draw a curve for each of them.
612It takes the vendors threshold as critical limit for the munin datafield.
613So you will see an alarm, if the value reaches the vendors threshold.
614
615Looking at the graph: It is a bad sign, if the curve starts
616to curl or to meander. The more horizontal it runs,
617the better. Of course it is normal, that the temperatures
618curve swings a bit. But the others should stay steady on
619their level if everything is ok.
620
621S.M.A.R.T. distinguishes between B<Pre-fail> and B<Old-age>
622Attributes. An old disk will have more curling curves
623because of degradation, especially for the B<Old-age> Attributes.
624You should then backup more often, run more selftests[1] and prepare
625the disks replacement.
626
627B<Act directly>, if a <Pre-Fail> Attribute goes below threshold.
628Immediately back-up your data and replace your hard disk drive.
629A failure may be imminent..
630
631[1] Consult the smartmontools manpages to learn about
632offline tests and automated selftests with smartd.
633Only with both activated, the values of the SMART-Attributes
634reflect the all over state of the device.
635
636Tutorials and articles about S.M.A.R.T. and smartmontools:
637http://smartmontools.sourceforge.net/doc.html#tutorials
638
639=head1 MAGIC MARKERS
640
641 #%# family=auto
642 #%# capabilities=autoconf suggest
643
644=head1 CALL OPTIONS
645
646B<none>
647
648=over 2
649
650Fetches values if called without arguments:
651
652E.g.: munin-run smart_hda
653
654=back
655
656B<config>
657
658=over 2
659
660Prints plugins configuration.
661
662E.g.: munin-run smart_hda config
663
664=back
665
666B<autoconf>
667
668=over 2
669
670Tries to find smartctl and outputs value 'yes' for success, 'no' if not.
671
672It's used by B<munin-node-configure> to see whether autoconfiguration is possible.
673
674=back
675
676B<suggest>
677
678=over 2
679
680Outputs the list of device names, that it found plugged to the system.
681
682B<munin-node-configure> use this to build the service links for this wildcard-plugin.
683
684=back
685
686=head1 VERSION
687
688Version 2.2
689
690=head1 BUGS
691
692None known
693
694=head1 AUTHOR
695
696(C) 2004-2009 Nicolas Stransky <Nico@stransky.cx>
697
698(C) 2008 Gabriele Pohl <contact@dipohl.de>
699Reformated existent documentation to POD-Style, added section Interpretation to the documentation.
700
701=head1 LICENSE
702
703GPLv2 (http://www.gnu.org/licenses/gpl-2.0.txt)
704
705=cut
706
707
708"""
709