]> git.ipfire.org Git - collecty.git/commitdiff
Add disk plugin
authorMichael Tremer <michael.tremer@ipfire.org>
Sun, 24 May 2015 10:38:05 +0000 (10:38 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Sun, 24 May 2015 10:47:10 +0000 (10:47 +0000)
The disk plugin collects information about read and written
sectors, IO operations, bad sectors and the temperature of
the disks.

It uses libatasmart to collect that information.

.gitignore
Makefile.am
autogen.sh
configure.ac
po/POTFILES.in
src/_collectymodule.c [new file with mode: 0644]
src/collecty/plugins/__init__.py
src/collecty/plugins/disk.py [new file with mode: 0644]

index 6256b1ba863344a024fdc943675a6a625abf6f4e..6e3c11c1dc3c1c1de6f1469be690e9f79b948152 100644 (file)
@@ -1,5 +1,6 @@
 /Makefile
 /build-aux
+/libtool
 /man/*.[0-9]
 /man/*.html
 /missing
 /*.tar.bz2
 /*.tar.gz
 /*.tar.xz
+*.a
+*.la
+*.lo
 *.log
 *.cache
 *.gmo
 *.mo
 *~
+.deps
+.dirstamp
+.libs
 Makefile.in
 aclocal.m4
 config.log
index 72c06ad46b9e327fe5ffe88870b82d578b75411c..b9aa84128802d7e13e136a2c7fef190ee69e6272 100644 (file)
@@ -87,6 +87,7 @@ collectyplugins_PYTHON = \
        src/collecty/plugins/base.py \
        src/collecty/plugins/conntrack.py \
        src/collecty/plugins/cpu.py \
+       src/collecty/plugins/disk.py \
        src/collecty/plugins/entropy.py \
        src/collecty/plugins/__init__.py \
        src/collecty/plugins/interface.py \
@@ -96,6 +97,27 @@ collectyplugins_PYTHON = \
 
 collectypluginsdir = $(collectydir)/plugins
 
+pkgpyexec_LTLIBRARIES = \
+       _collecty.la
+
+_collecty_la_SOURCES = \
+       src/_collectymodule.c
+
+_collecty_la_CFLAGS = \
+       $(AM_CFLAGS) \
+       $(LIBATASMART_CFLAGS) \
+       $(PYTHON_DEVEL_CFLAGS)
+
+_collecty_la_LDFLAGS = \
+       $(AM_LDFLAGS) \
+       -shared \
+       -module \
+       -avoid-version
+
+_collecty_la_LIBADD = \
+       $(LIBATASMART_LIBS) \
+       $(PYTHON_DEVEL_LIBS)
+
 dist_dbuspolicy_DATA = \
        src/dbus/org.ipfire.collecty1.conf
 
index 077e1095214c8bfceb5f01e0a469756bc847bc3f..212a394743dde20edf62483b2fb877aad527eb18 100755 (executable)
@@ -1,4 +1,5 @@
 #!/bin/sh
 
+libtoolize
 intltoolize --force --automake
 autoreconf --force --install --symlink
index 0d943e3a65761013c48b31c750c5383f89b0300e..810f6936c3f7c5e5a3976ed3592ecdecfa88db2c 100644 (file)
@@ -41,6 +41,8 @@ AM_INIT_AUTOMAKE([
        subdir-objects
 ])
 AM_SILENT_RULES([yes])
+LT_PREREQ(2.2)
+LT_INIT([disable-static])
 
 IT_PROG_INTLTOOL([0.40.0])
 
@@ -51,10 +53,20 @@ AC_PROG_LN_S
 AC_PROG_MKDIR_P
 AC_PROG_SED
 
+# C Compiler
+AC_PROG_CC
+AC_PROG_CC_C99
+AC_PROG_CC_C_O
+AC_PROG_GCC_TRADITIONAL
+
 AC_PATH_PROG([XSLTPROC], [xsltproc])
 
 # Python
 AM_PATH_PYTHON([2.7])
+PKG_CHECK_MODULES([PYTHON_DEVEL], [python-${PYTHON_VERSION}])
+
+# libatasmart
+PKG_CHECK_MODULES([LIBATASMART], [libatasmart >= 0.19])
 
 save_LIBS="$LIBS"
 
index 61715be212aaf21bb9cab5da6548af3300647b5e..b94ec757ce906946f5a475efa30bd3041f28d005 100644 (file)
@@ -9,6 +9,7 @@ src/collecty/ping.py
 src/collecty/plugins/base.py
 src/collecty/plugins/conntrack.py
 src/collecty/plugins/cpu.py
+src/collecty/plugins/disk.py
 src/collecty/plugins/entropy.py
 src/collecty/plugins/__init__.py
 src/collecty/plugins/interface.py
diff --git a/src/_collectymodule.c b/src/_collectymodule.c
new file mode 100644 (file)
index 0000000..eefe871
--- /dev/null
@@ -0,0 +1,325 @@
+/*
+ * collecty
+ * Copyright (C) 2015 IPFire Team (www.ipfire.org)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <Python.h>
+
+#include <atasmart.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/hdreg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <sys/ioctl.h>
+
+#define MODEL_SIZE  40
+#define SERIAL_SIZE 20
+
+typedef struct {
+       PyObject_HEAD
+       char* path;
+       struct hd_driveid identity;
+       SkDisk* disk;
+} BlockDevice;
+
+static void BlockDevice_dealloc(BlockDevice* self) {
+       if (self->disk)
+               sk_disk_free(self->disk);
+
+       if (self->path)
+               free(self->path);
+
+       self->ob_type->tp_free((PyObject*)self);
+}
+
+static int BlockDevice_get_identity(BlockDevice* device) {
+       int fd;
+
+       if ((fd = open(device->path, O_RDONLY | O_NONBLOCK)) < 0) {
+               return 1;
+       }
+
+       int r = ioctl(fd, HDIO_GET_IDENTITY, &device->identity);
+       close(fd);
+
+       if (r)
+               return 1;
+
+       return 0;
+}
+
+static int BlockDevice_smart_is_available(BlockDevice* device) {
+       SkBool available = FALSE;
+
+       int r = sk_disk_smart_is_available(device->disk, &available);
+       if (r)
+               return -1;
+
+       if (available)
+               return 0;
+
+       return 1;
+}
+
+static int BlockDevice_check_sleep_mode(BlockDevice* device) {
+       SkBool awake = FALSE;
+
+       int r = sk_disk_check_sleep_mode(device->disk, &awake);
+       if (r)
+               return -1;
+
+       if (awake)
+               return 0;
+
+       return 1;
+}
+
+static PyObject * BlockDevice_new(PyTypeObject* type, PyObject* args, PyObject* kwds) {
+       BlockDevice* self = (BlockDevice*)type->tp_alloc(type, 0);
+
+       if (self) {
+               self->path = NULL;
+
+               // libatasmart
+               self->disk = NULL;
+       }
+
+       return (PyObject *)self;
+}
+
+static int BlockDevice_init(BlockDevice* self, PyObject* args, PyObject* kwds) {
+       const char* path = NULL;
+
+       if (!PyArg_ParseTuple(args, "s", &path))
+               return -1;
+
+       self->path = strdup(path);
+
+       int r = BlockDevice_get_identity(self);
+       if (r) {
+               PyErr_Format(PyExc_OSError, "Could not open block device: %s", path);
+               return -1;
+       }
+
+       r = sk_disk_open(path, &self->disk);
+       if (r == 0) {
+               if (BlockDevice_smart_is_available(self) == 0) {
+                       if (BlockDevice_check_sleep_mode(self) == 0) {
+                               r = sk_disk_smart_read_data(self->disk);
+                               if (r) {
+                                       PyErr_Format(PyExc_OSError, "Could not open block device %s: %s", path,
+                                               strerror(errno));
+                                       return -1;
+                               }
+                       }
+               }
+       } else {
+               PyErr_Format(PyExc_OSError, "Could not open block device %s: %s", path,
+                       strerror(errno));
+               return -1;
+       }
+
+       //sk_disk_identify_is_available
+
+       return 0;
+}
+
+static PyObject* BlockDevice_get_path(PyObject* self) {
+       BlockDevice* device = (BlockDevice*)self;
+
+       return PyString_FromString(device->path);
+}
+
+static void clean_string(char *s) {
+       for (char* e = s; *e; e++) {
+               if (*e < ' ' || *e >= 127)
+                       *e = ' ';
+       }
+}
+
+static void drop_spaces(char *s) {
+       char *d = s;
+       bool prev_space = false;
+
+       s += strspn(s, " ");
+
+       for (; *s; s++) {
+               if (prev_space) {
+                       if (*s != ' ') {
+                               prev_space = false;
+                               *(d++) = ' ';
+                               *(d++) = *s;
+                       }
+               } else {
+                       if (*s == ' ')
+                               prev_space = true;
+                       else
+                               *(d++) = *s;
+               }
+       }
+
+       *d = 0;
+}
+
+static void copy_string(char* d, const char* s, size_t n) {
+       // Copy the source buffer to the destination buffer up to n
+       memcpy(d, s, n);
+
+       // Terminate the destination buffer with NULL
+       d[n] = '\0';
+
+       // Clean up the string from non-printable characters
+       clean_string(d);
+       drop_spaces(d);
+}
+
+static PyObject* BlockDevice_get_model(PyObject* self) {
+       BlockDevice* device = (BlockDevice*)self;
+
+       char model[MODEL_SIZE + 1];
+       copy_string(model, device->identity.model, sizeof(model));
+
+       return PyString_FromString(model);
+}
+
+static PyObject* BlockDevice_get_serial(PyObject* self) {
+       BlockDevice* device = (BlockDevice*)self;
+
+       char serial[SERIAL_SIZE + 1];
+       copy_string(serial, device->identity.serial_no, sizeof(serial));
+
+       return PyString_FromString(serial);
+}
+
+static PyObject* BlockDevice_is_smart_supported(PyObject* self) {
+       BlockDevice* device = (BlockDevice*)self;
+
+       if (BlockDevice_smart_is_available(device) == 0)
+               Py_RETURN_TRUE;
+
+       Py_RETURN_FALSE;
+}
+
+static PyObject* BlockDevice_is_awake(PyObject* self) {
+       BlockDevice* device = (BlockDevice*)self;
+
+       if (BlockDevice_check_sleep_mode(device) == 0)
+               Py_RETURN_TRUE;
+
+       Py_RETURN_FALSE;
+}
+
+static PyObject* BlockDevice_get_bad_sectors(PyObject* self) {
+       BlockDevice* device = (BlockDevice*)self;
+
+       if (BlockDevice_smart_is_available(device)) {
+               PyErr_Format(PyExc_OSError, "Device does not support SMART");
+               return NULL;
+       }
+
+       uint64_t bad_sectors;
+       int r = sk_disk_smart_get_bad(device->disk, &bad_sectors);
+       if (r)
+               return NULL;
+
+       return PyLong_FromUnsignedLongLong((unsigned long long)bad_sectors);
+}
+
+static PyObject* BlockDevice_get_temperature(PyObject* self) {
+       BlockDevice* device = (BlockDevice*)self;
+
+       if (BlockDevice_smart_is_available(device)) {
+               PyErr_Format(PyExc_OSError, "Device does not support SMART");
+               return NULL;
+       }
+
+       uint64_t mkelvin;
+       int r = sk_disk_smart_get_temperature(device->disk, &mkelvin);
+       if (r)
+               return NULL;
+
+       return PyLong_FromUnsignedLongLong((unsigned long long)mkelvin);
+}
+
+static PyGetSetDef BlockDevice_getsetters[] = {
+       {"path", (getter)BlockDevice_get_path, NULL, NULL, NULL},
+       {"model", (getter)BlockDevice_get_model, NULL, NULL, NULL},
+       {"serial", (getter)BlockDevice_get_serial, NULL, NULL, NULL},
+};
+
+static PyMethodDef BlockDevice_methods[] = {
+       {"get_bad_sectors", (PyCFunction)BlockDevice_get_bad_sectors, METH_NOARGS, NULL},
+       {"get_temperature", (PyCFunction)BlockDevice_get_temperature, METH_NOARGS, NULL},
+       {"is_smart_supported", (PyCFunction)BlockDevice_is_smart_supported, METH_NOARGS, NULL},
+       {"is_awake", (PyCFunction)BlockDevice_is_awake, METH_NOARGS, NULL},
+       {NULL}
+};
+
+static PyTypeObject BlockDeviceType = {
+       PyObject_HEAD_INIT(NULL)
+       0,                                  /*ob_size*/
+       "_collecty.BlockDevice",            /*tp_name*/
+       sizeof(BlockDevice),                /*tp_basicsize*/
+       0,                                  /*tp_itemsize*/
+       (destructor)BlockDevice_dealloc,    /*tp_dealloc*/
+       0,                                  /*tp_print*/
+       0,                                  /*tp_getattr*/
+       0,                                  /*tp_setattr*/
+       0,                                  /*tp_compare*/
+       0,                                  /*tp_repr*/
+       0,                                  /*tp_as_number*/
+       0,                                  /*tp_as_sequence*/
+       0,                                  /*tp_as_mapping*/
+       0,                                  /*tp_hash */
+       0,                                  /*tp_call*/
+       0,                                  /*tp_str*/
+       0,                                  /*tp_getattro*/
+       0,                                  /*tp_setattro*/
+       0,                                  /*tp_as_buffer*/
+       Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /*tp_flags*/
+       "BlockDevice objects",              /* tp_doc */
+       0,                                          /* tp_traverse */
+       0,                                          /* tp_clear */
+       0,                                          /* tp_richcompare */
+       0,                                          /* tp_weaklistoffset */
+       0,                                          /* tp_iter */
+       0,                                          /* tp_iternext */
+       BlockDevice_methods,                /* tp_methods */
+       0,                                  /* tp_members */
+       BlockDevice_getsetters,             /* tp_getset */
+       0,                                  /* tp_base */
+       0,                                  /* tp_dict */
+       0,                                  /* tp_descr_get */
+       0,                                  /* tp_descr_set */
+       0,                                  /* tp_dictoffset */
+       (initproc)BlockDevice_init,         /* tp_init */
+       0,                                  /* tp_alloc */
+       BlockDevice_new,                    /* tp_new */
+};
+
+static PyMethodDef collecty_module_methods[] = {
+       {NULL},
+};
+
+void init_collecty(void) {
+       if (PyType_Ready(&BlockDeviceType) < 0)
+               return;
+
+       PyObject* m = Py_InitModule("_collecty", collecty_module_methods);
+
+       PyModule_AddObject(m, "BlockDevice", (PyObject*)&BlockDeviceType);
+}
index cc7f41ce544d36859ca74b7c801857fcd8e2796a..5f843a887df910aab72a02a62ad46277aa1f0bda 100644 (file)
@@ -24,6 +24,7 @@ from base import Timer, get
 import base
 import conntrack
 import cpu
+import disk
 import entropy
 import interface
 import latency
diff --git a/src/collecty/plugins/disk.py b/src/collecty/plugins/disk.py
new file mode 100644 (file)
index 0000000..b5a2346
--- /dev/null
@@ -0,0 +1,315 @@
+#!/usr/bin/python
+###############################################################################
+#                                                                             #
+# collecty - A system statistics collection daemon for IPFire                 #
+# Copyright (C) 2012 IPFire development team                                  #
+#                                                                             #
+# This program is free software: you can redistribute it and/or modify        #
+# it under the terms of the GNU General Public License as published by        #
+# the Free Software Foundation, either version 3 of the License, or           #
+# (at your option) any later version.                                         #
+#                                                                             #
+# This program is distributed in the hope that it will be useful,             #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
+# GNU General Public License for more details.                                #
+#                                                                             #
+# You should have received a copy of the GNU General Public License           #
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
+#                                                                             #
+###############################################################################
+
+from collecty import _collecty
+import os
+import re
+
+import base
+
+from ..i18n import _
+
+class GraphTemplateDiskBadSectors(base.GraphTemplate):
+       name = "disk-bad-sectors"
+
+       rrd_graph = [
+               "DEF:bad_sectors=%(file)s:bad_sectors:AVERAGE",
+
+               "AREA:bad_sectors#ff0000:%s" % _("Bad Sectors"),
+
+               "VDEF:bad_sectors_cur=bad_sectors,LAST",
+               "VDEF:bad_sectors_max=bad_sectors,MAXIMUM",
+               "GPRINT:bad_sectors_cur:%12s\:" % _("Current") + " %9.2lf",
+               "GPRINT:bad_sectors_max:%12s\:" % _("Maximum") + " %9.2lf\\n",
+       ]
+
+       @property
+       def graph_title(self):
+               return _("Bad Sectors of %s") % self.object.device_string
+
+       @property
+       def graph_vertical_label(self):
+               return _("Pending/Relocated Sectors")
+
+
+class GraphTemplateDiskBytes(base.GraphTemplate):
+       name = "disk-bytes"
+
+       rrd_graph = [
+               "DEF:read_sectors=%(file)s:read_sectors:AVERAGE",
+               "DEF:write_sectors=%(file)s:write_sectors:AVERAGE",
+
+               "CDEF:read_bytes=read_sectors,512,*",
+               "CDEF:write_bytes=write_sectors,512,*",
+
+               "LINE1:read_bytes#ff0000:%-15s" % _("Read"),
+               "VDEF:read_cur=read_bytes,LAST",
+               "VDEF:read_min=read_bytes,MINIMUM",
+               "VDEF:read_max=read_bytes,MAXIMUM",
+               "VDEF:read_avg=read_bytes,AVERAGE",
+               "GPRINT:read_cur:%12s\:" % _("Current") + " %9.2lf",
+               "GPRINT:read_max:%12s\:" % _("Maximum") + " %9.2lf",
+               "GPRINT:read_min:%12s\:" % _("Minimum") + " %9.2lf",
+               "GPRINT:read_avg:%12s\:" % _("Average") + " %9.2lf\\n",
+
+               "LINE1:write_bytes#00ff00:%-15s" % _("Written"),
+               "VDEF:write_cur=write_bytes,LAST",
+               "VDEF:write_min=write_bytes,MINIMUM",
+               "VDEF:write_max=write_bytes,MAXIMUM",
+               "VDEF:write_avg=write_bytes,AVERAGE",
+               "GPRINT:write_cur:%12s\:" % _("Current") + " %9.2lf",
+               "GPRINT:write_max:%12s\:" % _("Maximum") + " %9.2lf",
+               "GPRINT:write_min:%12s\:" % _("Minimum") + " %9.2lf",
+               "GPRINT:write_avg:%12s\:" % _("Average") + " %9.2lf\\n",
+       ]
+
+       lower_limit = 0
+
+       @property
+       def graph_title(self):
+               return _("Disk Utilisation of %s") % self.object.device_string
+
+       @property
+       def graph_vertical_label(self):
+               return _("Byte per Second")
+
+
+class GraphTemplateDiskIoOps(base.GraphTemplate):
+       name = "disk-io-ops"
+
+       rrd_graph = [
+               "DEF:read_ios=%(file)s:read_ios:AVERAGE",
+               "DEF:write_ios=%(file)s:write_ios:AVERAGE",
+
+               "LINE1:read_ios#ff0000:%-15s" % _("Read"),
+               "VDEF:read_cur=read_ios,LAST",
+               "VDEF:read_min=read_ios,MINIMUM",
+               "VDEF:read_max=read_ios,MAXIMUM",
+               "VDEF:read_avg=read_ios,AVERAGE",
+               "GPRINT:read_cur:%12s\:" % _("Current") + " %6.2lf",
+               "GPRINT:read_max:%12s\:" % _("Maximum") + " %6.2lf",
+               "GPRINT:read_min:%12s\:" % _("Minimum") + " %6.2lf",
+               "GPRINT:read_avg:%12s\:" % _("Average") + " %6.2lf\\n",
+
+               "LINE1:write_ios#00ff00:%-15s" % _("Written"),
+               "VDEF:write_cur=write_ios,LAST",
+               "VDEF:write_min=write_ios,MINIMUM",
+               "VDEF:write_max=write_ios,MAXIMUM",
+               "VDEF:write_avg=write_ios,AVERAGE",
+               "GPRINT:write_cur:%12s\:" % _("Current") + " %6.2lf",
+               "GPRINT:write_max:%12s\:" % _("Maximum") + " %6.2lf",
+               "GPRINT:write_min:%12s\:" % _("Minimum") + " %6.2lf",
+               "GPRINT:write_avg:%12s\:" % _("Average") + " %6.2lf\\n",
+       ]
+
+       lower_limit = 0
+
+       @property
+       def graph_title(self):
+               return _("Disk IO Operations of %s") % self.object.device_string
+
+       @property
+       def graph_vertical_label(self):
+               return _("Operations per Second")
+
+
+class GraphTemplateDiskTemperature(base.GraphTemplate):
+       name = "disk-temperature"
+
+       rrd_graph = [
+               "DEF:mkelvin=%(file)s:temperature:AVERAGE",
+               "CDEF:celsius=mkelvin,1000,/,273.15,-",
+
+               "LINE2:celsius#ff0000:%s" % _("Temperature"),
+               "VDEF:temp_cur=celsius,LAST",
+               "VDEF:temp_min=celsius,MINIMUM",
+               "VDEF:temp_max=celsius,MAXIMUM",
+               "VDEF:temp_avg=celsius,AVERAGE",
+               "GPRINT:temp_cur:%12s\:" % _("Current") + " %3.2lf",
+               "GPRINT:temp_max:%12s\:" % _("Maximum") + " %3.2lf",
+               "GPRINT:temp_min:%12s\:" % _("Minimum") + " %3.2lf",
+               "GPRINT:temp_avg:%12s\:" % _("Average") + " %3.2lf\\n",
+       ]
+
+       @property
+       def graph_title(self):
+               return _("Disk Temperature of %s") % self.object.device_string
+
+       @property
+       def graph_vertical_label(self):
+               return _("° Celsius")
+
+       @property
+       def rrd_graph_args(self):
+               return [
+                       # Make the y-axis have a decimal
+                       "--left-axis-format", "%3.1lf",
+               ]
+
+
+class DiskObject(base.Object):
+       rrd_schema = [
+               "DS:awake:GAUGE:0:1",
+               "DS:read_ios:DERIVE:0:U",
+               "DS:read_sectors:DERIVE:0:U",
+               "DS:write_ios:DERIVE:0:U",
+               "DS:write_sectors:DERIVE:0:U",
+               "DS:bad_sectors:GAUGE:0:U",
+               "DS:temperature:GAUGE:U:U",
+       ]
+
+       def __repr__(self):
+               return "<%s %s (%s)>" % (self.__class__.__name__, self.sys_path, self.id)
+
+       def init(self, device):
+               self.dev_path = os.path.join("/dev", device)
+               self.sys_path = os.path.join("/sys/block", device)
+
+               self.device = _collecty.BlockDevice(self.dev_path)
+
+       @property
+       def id(self):
+               return "-".join((self.device.model, self.device.serial))
+
+       @property
+       def device_string(self):
+               return "%s (%s)" % (self.device.model, self.dev_path)
+
+       def collect(self):
+               stats = self.parse_stats()
+
+               return ":".join((
+                       self.is_awake(),
+                       stats.get("read_ios"),
+                       stats.get("read_sectors"),
+                       stats.get("write_ios"),
+                       stats.get("write_sectors"),
+                       self.get_bad_sectors(),
+                       self.get_temperature(),
+               ))
+
+       def parse_stats(self):
+               """
+                       https://www.kernel.org/doc/Documentation/block/stat.txt
+
+                       Name            units         description
+                       ----            -----         -----------
+                       read I/Os       requests      number of read I/Os processed
+                       read merges     requests      number of read I/Os merged with in-queue I/O
+                       read sectors    sectors       number of sectors read
+                       read ticks      milliseconds  total wait time for read requests
+                       write I/Os      requests      number of write I/Os processed
+                       write merges    requests      number of write I/Os merged with in-queue I/O
+                       write sectors   sectors       number of sectors written
+                       write ticks     milliseconds  total wait time for write requests
+                       in_flight       requests      number of I/Os currently in flight
+                       io_ticks        milliseconds  total time this block device has been active
+                       time_in_queue   milliseconds  total wait time for all requests
+               """
+               stats_file = os.path.join(self.sys_path, "stat")
+
+               with open(stats_file) as f:
+                       stats = f.read().split()
+
+                       return {
+                               "read_ios"      : stats[0],
+                               "read_merges"   : stats[1],
+                               "read_sectors"  : stats[2],
+                               "read_ticks"    : stats[3],
+                               "write_ios"     : stats[4],
+                               "write_merges"  : stats[5],
+                               "write_sectors" : stats[6],
+                               "write_ticks"   : stats[7],
+                               "in_flight"     : stats[8],
+                               "io_ticks"      : stats[9],
+                               "time_in_queue" : stats[10],
+                       }
+
+       def is_smart_supported(self):
+               """
+                       We can only query SMART data if SMART is supported by the disk
+                       and when the disk is awake.
+               """
+               return self.device.is_smart_supported() and self.device.is_awake()
+
+       def is_awake(self):
+               # If SMART is supported we can get the data from the disk
+               if self.device.is_smart_supported():
+                       if self.device.is_awake():
+                               return 1
+                       else:
+                               return 0
+
+               # Otherwise we just assume that the disk is awake
+               return 1
+
+       def get_temperature(self):
+               if not self.is_smart_supported():
+                       return "NaN"
+
+               return self.device.get_temperature()
+
+       def get_bad_sectors(self):
+               if not self.is_smart_supported():
+                       return "NaN"
+
+               return self.device.get_bad_sectors()
+
+
+class DiskPlugin(base.Plugin):
+       name = "disk"
+       description = "Disk Plugin"
+
+       templates = [
+               GraphTemplateDiskBadSectors,
+               GraphTemplateDiskBytes,
+               GraphTemplateDiskIoOps,
+               GraphTemplateDiskTemperature,
+       ]
+
+       block_device_patterns = [
+               re.compile(r"(x?v|s)d[a-z]+"),
+               re.compile(r"mmcblk[0-9]+"),
+       ]
+
+       @property
+       def objects(self):
+               for dev in self.find_block_devices():
+                       try:
+                               yield DiskObject(self, dev)
+                       except OSError:
+                               pass
+
+       def find_block_devices(self):
+               for device in os.listdir("/sys/block"):
+                       # Skip invalid device names
+                       if not self._valid_block_device_name(device):
+                               continue
+
+                       yield device
+
+       def _valid_block_device_name(self, name):
+               # Check if the given name matches any of the valid patterns.
+               for pattern in self.block_device_patterns:
+                       if pattern.match(name):
+                               return True
+
+               return False