diff --git a/Makefile.am b/Makefile.am index 5a88d5402383b1851ebcc1c5149b1735285af958..e05a3361b55de3e73e28984fe333b425b2365a1c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 index 0000000000000000000000000000000000000000..65c963c7eb450efd7633bf3e7ec0cb90dffeda51 --- /dev/null +++ b/lib/storage/filestorage.py @@ -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 index 0000000000000000000000000000000000000000..03b03b2a1c8cb65ef96f76f957007c2abbe5c26d --- /dev/null +++ b/test/py/ganeti.storage.filestorage_unittest.py @@ -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()