Backend function for file storage space reporting
authorHelga Velroyen <helgav@google.com>
Tue, 14 May 2013 13:23:32 +0000 (15:23 +0200)
committerHelga Velroyen <helgav@google.com>
Tue, 14 May 2013 19:17:30 +0000 (21:17 +0200)
This adds functionality to retrieve disk space information
for file storage. It calls the 'df' tool and parses its
output to extract the total and free amount of disk space
on the disk where the given path is located.
The code is not integrated yet, but thoroughly unit-tested.

Signed-off-by: Helga Velroyen <helgav@google.com>
Reviewed-by: Bernardo dal Seno <bdalseno@google.com>

Makefile.am
lib/storage/filestorage.py [new file with mode: 0644]
test/py/ganeti.storage.filestorage_unittest.py [new file with mode: 0755]

index 5a88d54..e05a336 100644 (file)
@@ -322,7 +322,8 @@ storage_PYTHON = \
        lib/storage/container.py \
        lib/storage/drbd.py \
        lib/storage/drbd_info.py \
-       lib/storage/drbd_cmdgen.py
+       lib/storage/drbd_cmdgen.py \
+       lib/storage/filestorage.py
 
 rapi_PYTHON = \
        lib/rapi/__init__.py \
@@ -1191,6 +1192,7 @@ python_tests = \
        test/py/ganeti.server.rapi_unittest.py \
        test/py/ganeti.ssconf_unittest.py \
        test/py/ganeti.ssh_unittest.py \
+       test/py/ganeti.storage.filestorage_unittest.py \
        test/py/ganeti.tools.burnin_unittest.py \
        test/py/ganeti.tools.ensure_dirs_unittest.py \
        test/py/ganeti.tools.node_daemon_setup_unittest.py \
diff --git a/lib/storage/filestorage.py b/lib/storage/filestorage.py
new file mode 100644 (file)
index 0000000..65c963c
--- /dev/null
@@ -0,0 +1,78 @@
+#
+#
+
+# Copyright (C) 2013 Google Inc.
+#
+# 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 2 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, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+
+"""File storage functions.
+
+"""
+
+from ganeti import errors
+from ganeti import utils
+
+DF_M_UNIT = 'M'
+DF_MIN_NUM_COLS = 4
+DF_NUM_LINES = 2
+
+
+def _ParseDfResult(dfresult):
+  """Parses the output of the call of the 'df' tool.
+
+     @type dfresult: string
+     @param dfresult: output of the 'df' call
+     @return: tuple (size, free) of the total and free disk space in MebiBytes
+  """
+  df_lines = dfresult.splitlines()
+  if len(df_lines) != DF_NUM_LINES:
+    raise errors.CommandError("'df' output has wrong number of lines: %s" %
+                              len(df_lines))
+  df_values = df_lines[1].strip().split()
+  if len(df_values) < DF_MIN_NUM_COLS:
+    raise errors.CommandError("'df' output does not have enough columns: %s" %
+                              len(df_values))
+  size_str = df_values[1]
+  if size_str[-1] != DF_M_UNIT:
+    raise errors.CommandError("'df': 'size' not given in Mebibytes.")
+  free_str = df_values[3]
+  if free_str[-1] != DF_M_UNIT:
+    raise errors.CommandError("'df': 'free' not given in Mebibytes.")
+  size = int(size_str[:-1])
+  free = int(free_str[:-1])
+  return (size, free)
+
+
+def GetSpaceInfo(path, _parsefn=_ParseDfResult):
+  """Retrieves the free and total space of the device where the file is
+     located.
+
+     @type path: string
+     @param path: Path of the file whose embracing device's capacity is
+       reported.
+     @type _parsefn: function
+     @param _parsefn: Function that parses the output of the 'df' command;
+       given as parameter to make this code more testable.
+     @return: a dictionary containing 'vg_size' and 'vg_free'
+  """
+  cmd = ['df', '-BM', path]
+  result = utils.RunCmd(cmd)
+  if result.failed:
+    raise errors.CommandError("Failed to run 'df' command: %s - %s" %
+                              (result.fail_reason, result.output))
+  (size, free) = _parsefn(result.stdout)
+  return {"vg_size": size, "vg_free": free}
diff --git a/test/py/ganeti.storage.filestorage_unittest.py b/test/py/ganeti.storage.filestorage_unittest.py
new file mode 100755 (executable)
index 0000000..03b03b2
--- /dev/null
@@ -0,0 +1,94 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2013 Google Inc.
+#
+# 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 2 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, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+
+"""Script for unittesting the ganeti.storage.file module"""
+
+
+import unittest
+
+from ganeti import errors
+from ganeti.storage import filestorage
+
+import testutils
+
+
+class TestFileStorageSpaceInfo(unittest.TestCase):
+
+  def testSpaceInfoPathInvalid(self):
+    """Tests that an error is raised when the given file is not existing.
+
+    """
+    self.assertRaises(errors.CommandError, filestorage.GetSpaceInfo,
+                      "/path/does/not/exist/")
+
+  def testSpaceInfoPathValid(self):
+    """Tests that the 'df' command is run if the file is valid.
+
+    """
+    info = filestorage.GetSpaceInfo("/")
+
+  def testParseDfOutputValidInput(self):
+    """Tests that parsing of the output of 'df' works correctly.
+
+    """
+    valid_df_output = \
+      "Filesystem             1M-blocks   Used Available Use% Mounted on\n" \
+      "/dev/mapper/sysvg-root   161002M 58421M    94403M  39% /"
+    expected_size = 161002
+    expected_free = 94403
+
+    (size, free) = filestorage._ParseDfResult(valid_df_output)
+    self.assertEqual(expected_size, size,
+                     "Calculation of total size is incorrect.")
+    self.assertEqual(expected_free, free,
+                     "Calculation of free space is incorrect.")
+
+
+  def testParseDfOutputInvalidInput(self):
+    """Tests that parsing of the output of 'df' works correctly when invalid
+       input is given.
+
+    """
+    invalid_output_header_missing = \
+      "/dev/mapper/sysvg-root   161002M 58421M    94403M  39% /"
+    invalid_output_dataline_missing = \
+      "Filesystem             1M-blocks   Used Available Use% Mounted on\n"
+    invalid_output_wrong_num_columns = \
+      "Filesystem             1M-blocks Available\n" \
+      "/dev/mapper/sysvg-root   161002M    94403M"
+    invalid_output_units_wrong = \
+      "Filesystem             1M-blocks   Used Available Use% Mounted on\n" \
+      "/dev/mapper/sysvg-root   161002G 58421G    94403G  39% /"
+    invalid_output_units_missing = \
+      "Filesystem             1M-blocks   Used Available Use% Mounted on\n" \
+      "/dev/mapper/sysvg-root    161002  58421     94403  39% /"
+    invalid_outputs = [invalid_output_header_missing,
+                       invalid_output_dataline_missing,
+                       invalid_output_wrong_num_columns,
+                       invalid_output_units_wrong,
+                       invalid_output_units_missing]
+
+    for output in invalid_outputs:
+      self.assertRaises(errors.CommandError, filestorage._ParseDfResult, output)
+
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram()