diff --git a/controls/Makefile.am b/controls/Makefile.am index 68a32f14..1222175f 100644 --- a/controls/Makefile.am +++ b/controls/Makefile.am @@ -29,6 +29,7 @@ controls_PYTHON = \ sanadapters.py \ sensors.py \ users.py \ + filesystems.py \ $(NULL) controlsdir = $(pythondir)/wok/plugins/ginger/controls diff --git a/controls/__init__.py b/controls/__init__.py index ee80635b..fe5a6a1c 100644 --- a/controls/__init__.py +++ b/controls/__init__.py @@ -26,6 +26,7 @@ from sanadapters import SanAdapters from sensors import Sensors from users import Users +from filesystems import FileSystems __all__ = [ Backup, @@ -36,5 +37,6 @@ SanAdapters, Sensors, Sep, - Users + Users, + FileSystems ] diff --git a/controls/filesystems.py b/controls/filesystems.py new file mode 100644 index 00000000..9252cf58 --- /dev/null +++ b/controls/filesystems.py @@ -0,0 +1,56 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013-2014 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +from wok.control.base import Collection, Resource + + + +class FileSystems(Collection): + """ + Collections representing the filesystems on the system + """ + def __init__(self, model): + super(FileSystems, self).__init__(model) + self.role_key = 'host' + self.admin_methods = ['GET', 'POST', 'DELETE'] + self.resource = FileSystem + + +class FileSystem(Resource): + """ + Resource representing a single file system + """ + def __init__(self, model, ident): + super(FileSystem, self).__init__(model, ident) + self.role_key = 'host' + self.admin_methods = ['GET', 'POST', 'DELETE'] + self.uri_fmt = "/filesystems/%s" + self.format = self.generate_action_handler_task('format', ['disk']) + + @property + def data(self): + return {'filesystem': self.info['filesystem'], + 'type': self.info['type'], + 'size': self.info['size'], + 'used': self.info['used'], + 'avail': self.info['avail'], + 'use%': self.info['use%'], + 'mounted_on': self.info['mounted_on']} + + diff --git a/docs/API.md b/docs/API.md index 7565a0c5..cff7186a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -147,3 +147,34 @@ to use the API effectively, please consider the following general conventions * start: Start the SEP daemon on host server. * stop: Stop the SEP daemon on host server. +### Collection: File Systems + +URI: /plugins/ginger/filesystems + +**Methods:** + +* **GET**: Retrieve a summarized list of all mounted filesystems + +* **POST**: Mount a file system + * blk_dev : Path of the device to be mounted. + * mount_point : Mount point for the filesystem + +### Resource: File System + +URI: /plugins/ginger/filesystems/*:mount_point* + +**Methods:** + +* **GET**: Retrieve the full description of the mounted filesystem + + * use%: Percentage of the filesystem used + * used: Amount of space used in filesystem + * size: Total size of the filesystem + * mounted_on: Mount point of the filesystem + * avail : Total space available on the filesystem + * device_name : Backing device name of the filesystem. + * type : Filesystem type. + +* **DELETE**: Unmount the Filesystem + + diff --git a/ginger.py b/ginger.py index 8fc8335c..999586c8 100644 --- a/ginger.py +++ b/ginger.py @@ -21,7 +21,7 @@ import os from controls import Backup, Capabilities, Firmware, Network, PowerProfiles -from controls import SanAdapters, Sensors, Sep, Users +from controls import SanAdapters, Sensors, Sep, Users, FileSystems from i18n import messages from wok.config import PluginPaths from wok.root import WokRoot @@ -38,6 +38,7 @@ def __init__(self, wok_options=None): self.powerprofiles = PowerProfiles(self.model) self.sensors = Sensors(self.model) self.users = Users(self.model) + self.filesystems = FileSystems(self.model) self.network = Network(self.model) self.api_schema = json.load(open(os.path.join(os.path.dirname( os.path.abspath(__file__)), 'API.json'))) diff --git a/models/Makefile.am b/models/Makefile.am index bd189300..8d79403f 100644 --- a/models/Makefile.am +++ b/models/Makefile.am @@ -31,6 +31,8 @@ models_PYTHON = \ sanadapters.py \ sensors.py \ users.py \ + filesystem.py \ + fs_utils.py \ $(NULL) modelsdir = $(pythondir)/wok/plugins/ginger/models diff --git a/models/filesystem.py b/models/filesystem.py new file mode 100644 index 00000000..2a9ff47f --- /dev/null +++ b/models/filesystem.py @@ -0,0 +1,81 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2014 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +from wok.exception import OperationFailed, NotFoundError +import libvirt +import fs_utils + + +class FileSystemsModel(object): + """ + Model class for listing filesystems (df -hT) and mounting a filesystem + """ + def __init__(self, **kargs): + pass + + def create(self, params): + try: + blk_dev = params['blk_dev'] + mount_point = params['mount_point'] + persistent = params['persistent'] + fs_utils._mount_a_blk_device(blk_dev, mount_point) + if persistent: + fs_utils.make_persist(blk_dev, mount_point) + except libvirt.libvirtError as e: + raise OperationFailed("KCHFS00000", + {'mount point': mount_point, 'err': e.get_error_message()}) + return mount_point + + def get_list(self): + try: + fs_names = fs_utils._get_fs_names() + + except libvirt.libvirtError as e: + raise OperationFailed("KCHFS00001", + {'err': e.get_error_message()}) + + return fs_names + + + +class FileSystemModel(object): + """ + Model for viewing and unmounting the filesystem + """ + def __init__(self, **kargs): + pass + + + def lookup(self, name): + try: + return fs_utils._get_fs_info(name) + + except ValueError: + raise NotFoundError("KCHFS00002", {'name': name}) + + def delete(self, name): + try: + fs_utils._umount_partition(name) + fs_utils.remove_persist(name) + except libvirt.libvirtError as e: + raise OperationFailed("KCHFS00003", + {'err': e.get_error_message()}) + + + diff --git a/models/fs_utils.py b/models/fs_utils.py new file mode 100644 index 00000000..fa238a73 --- /dev/null +++ b/models/fs_utils.py @@ -0,0 +1,294 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013-2015 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import os.path +import re +import subprocess + + +from wok.exception import OperationFailed + + +def _parse_df_output(output): + """ + This method parses the output of 'df -hT' command. + :param output: Parsed output of the 'df -hT' command + :return:list containing filesystems information + """ + + try: + output = output.splitlines() + except: + raise OperationFailed("KCHDISKS00032", "Invalid df output provided") + + out_list = [] + + try: + for fs in output[1:]: + fs_dict = {} + fs_list = fs.split() + fs_dict['filesystem'] = fs_list[0] + fs_dict['type'] = fs_list[1] + fs_dict['size'] = fs_list[2] + fs_dict['used'] = fs_list[3] + fs_dict['avail'] = fs_list[4] + fs_dict['use%'] = fs_list[5] + fs_dict['mounted_on'] = fs_list[6] + + out_list.append(fs_dict) + except: + raise OperationFailed("KCHDISKS00033", "Parsing the output failed.") + + return out_list + + +def _get_fs_names(): + """ + Fetches list of filesystems + :return: list of filesystem names + """ + fs_name_list = [] + try: + outlist = _get_df_output() + fs_name_list = [d['mounted_on'] for d in outlist] + return fs_name_list + except: + raise OperationFailed("KCHDISKS00031", "error in fetching the list of filesystems") + + +def _get_fs_info(mnt_pt): + """ + Fetches information about the given filesystem + :param mnt_pt: mount point of the filesystem + :return: dictionary containing filesystem info + """ + fs_info = {} + try: + fs_search_list = _get_df_output() + for i in fs_search_list: + if mnt_pt == i['mounted_on']: + fs_info['filesystem'] = i['filesystem'] + fs_info['type'] = i['type'] + fs_info['size'] = i['size'] + fs_info['used'] = i['used'] + fs_info['avail'] = i['avail'] + fs_info['use%'] = i['use%'] + fs_info['mounted_on'] = i['mounted_on'] + except: + raise OperationFailed("KCHDISKS00030", {'device': mnt_pt}) + return fs_info + +def _get_swapdev_list_parser(output): + """ + This method parses the output of 'cat /proc/swaps' command + :param output: output of 'cat /proc/swaps' command + :return:list of swap devices + """ + output = output.splitlines() + output_list = [] + + for swapdev in output[1:]: + dev_name = swapdev.split()[0] + output_list.append(dev_name) + + return output_list + +def _create_file(size, file_loc): + """ + This method creates a flat file + :param size: size of the flat file to be created + :param file_loc: location of the flat file + :return: + """ + crt_out = subprocess.Popen(["dd", "if=/dev/zero", "of=" + file_loc, "bs=" + size, "count=1"], stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + out, err = crt_out.communicate() + if crt_out.returncode != 0: + raise OperationFailed("KCHDISKS0001E", {'err': err}) + return + +def _make_swap(file_loc): + """ + This method configures the given file/device as a swap file/device + :param file_loc: path of the file/device + :return: + """ + os.chown(file_loc,0,0) + os.chmod(file_loc, 0600) + mkswp_out = subprocess.Popen(["mkswap", file_loc], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = mkswp_out.communicate() + if mkswp_out.returncode != 0: + raise OperationFailed("KCHDISKS0001E", {'err': err}) + return + +def _activate_swap(file_loc): + """ + This method activates the swap file/device + :param file_loc: path of the file/device + :return: + """ + swp_out = subprocess.Popen(["swapon", file_loc], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = swp_out.communicate() + if swp_out.returncode != 0: + raise OperationFailed("KCHDISKS0001E", {'err': err}) + return + +def _makefs(fstype, name): + """ + This method formats the device with the specified file system type + :param fstype: type of the filesystem + :param name: path of the device/partition to be formatted + :return: + """ + fs_out = subprocess.Popen(["mkfs", "-t", fstype ,"-F", name], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = fs_out.communicate() + if fs_out.returncode != 0: + raise OperationFailed("KCHDISKS0001E", {'err': err}) + return + +def _parse_swapon_output(output): + """ + This method parses the output of 'grep -w devname /proc/swaps' + :param output: output of 'grep -w devname /proc/swaps' command + :return: dictionary containing swap device info + """ + output_dict = {} + output_list = output.split() + output_dict['filename'] = output_list[0] + output_dict['type'] = output_list[1] + output_dict['size'] = output_list[2] + output_dict['used'] = output_list[3] + output_dict['priority'] = output_list[4] + + return output_dict + +def _get_swap_output(device_name): + """ + This method executes the command 'grep -w devname /proc/swaps' + :param device_name: name of the swap device + :return: + """ + swap_out = subprocess.Popen( + ["grep", "-w", device_name, "/proc/swaps"] , + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = swap_out.communicate() + if swap_out.returncode != 0: + raise OperationFailed("KCHDISKS0001E", {'err': err}) + + return _parse_swapon_output(out) + +def _swapoff_device(device_name): + """ + This method removes swap devices + :param device_name: path of the swap device to be removed + :return: + """ + swapoff_out = subprocess.Popen( + ["swapoff", device_name] , + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = swapoff_out.communicate() + if swapoff_out.returncode != 0: + raise OperationFailed("KCHDISKS0001E", {'err': err}) + else: + os.remove(device_name) if os.path.exists(device_name) else None + +def _is_mntd(partition_name): + """ + This method checks if the partition is mounted + :param partition_name: name of the partition + :return: + """ + is_mntd_out = subprocess.Popen( + ["grep", "-w", "^/dev/"+partition_name+"\s", "/proc/mounts"] , + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = is_mntd_out.communicate() + if is_mntd_out.returncode != 0: + return False + else: + return True + + +def _get_df_output(): + """ + Executes 'df -hT' command and returns + :return: output of 'df -hT' command + """ + dfh_out = subprocess.Popen(["df", "-hT"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + fs_out, fs_err = dfh_out.communicate() + + if dfh_out.returncode != 0: + raise OperationFailed("KCHDISKS0002F", {'err': fs_err}) + return _parse_df_output(fs_out) + + +def _mount_a_blk_device(blk_dev, mount_point): + """ + Mounts the given block device on the given mount point + :param blk_dev: path of the block device + :param mount_point: mount point + :return: + """ + mount_out = subprocess.Popen(["/bin/mount", blk_dev, mount_point], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + std_out, std_err = mount_out.communicate() + if mount_out.returncode != 0: + raise OperationFailed("KCHDISKS00034", {'err': std_err}) + + +def _umount_partition(mnt_pt): + """ + Unmounts the given mount point (filesystem) + :param mnt_pt: mount point + :return: + """ + umount_out = subprocess.Popen(["/bin/umount", mnt_pt], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + std_out, std_err = umount_out.communicate() + if umount_out.returncode != 0: + raise OperationFailed("KCHDISKS00035", {'err': std_err}) + return + + +def make_persist(dev, mntpt): + """ + This method persists the mounted filesystem by making an entry in fstab + :param dev: path of the device to be mounted + :param mntpt: mount point + :return: + """ + fo = open("/etc/fstab", "a+") + fo.write(dev + " " + mntpt + " " + "defaults 1 2") + fo.close() + +def remove_persist(mntpt): + """ + This method removes the fstab entry + :param mntpt: mount point + :return: + """ + fo = open("/etc/fstab", "r") + lines = fo.readlines() + output = [] + fo.close() + fo = open("/etc/fstab", "w") + for line in lines: + if mntpt in line: + continue + else: + output.append(line) + fo.writelines(output) + fo.close() diff --git a/models/model.py b/models/model.py index bd6c0eeb..801e0929 100644 --- a/models/model.py +++ b/models/model.py @@ -27,6 +27,8 @@ from sanadapters import SanAdapterModel, SanAdaptersModel from sensors import SensorsModel from users import UsersModel, UserModel +from filesystem import FileSystemsModel, FileSystemModel + from wok import config from wok.basemodel import BaseModel @@ -47,6 +49,8 @@ def __init__(self): user = UserModel() interfaces = InterfacesModel() interface = InterfaceModel() + filesystems = FileSystemsModel() + filesystem = FileSystemModel() network = NetworkModel() archives = ArchivesModel(objstore=self._objstore) archive = ArchiveModel(objstore=self._objstore) @@ -60,13 +64,14 @@ def __init__(self): subscriber = SubscribersModel() features = [firmware, backup, network, powerprofiles, san_adapters, - sensors, ibm_sep, users] + sensors, ibm_sep, users, filesystems] capabilities = CapabilitiesModel(features) sub_models = [ backup, archives, archive, firmware, interfaces, interface, + filesystems, filesystem, network, powerprofiles, powerprofile, users, user, diff --git a/tests/test_filesystems.py b/tests/test_filesystems.py new file mode 100644 index 00000000..83e9dd7c --- /dev/null +++ b/tests/test_filesystems.py @@ -0,0 +1,67 @@ +# +# Project Ginger +# +# Copyright IBM, Corp. 2015 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import crypt +import spwd +import unittest + +import models.filesystem as filesystem + +from wok.exception import OperationFailed +from wok.rollbackcontext import RollbackContext + + +class FileSystemTests(unittest.TestCase): + + def test_get_fs_list(self): + fs = filesystem.FileSystemsModel() + fs_list = fs.get_list() + self.assertGreaterEqual(len(fs_list), 0) + + def test_mount_fs(self): + fs = filesystem.FileSystemsModel() + fsd = filesystem.FileSystemModel() + blkdev = '/testfile' + mntpt = '/test' + persistent = False + + fs_list = fs.get_list() + with RollbackContext() as rollback: + fs.create({'blk_dev':blkdev, 'mount_point':mntpt, 'persistent':persistent}) + rollback.prependDefer(fsd.delete, mntpt) + + new_fs_list = fs.get_list() + self.assertEqual(len(new_fs_list), len(fs_list) + 1) + + + def test_mount_existing_fs_fails(self): + fs = filesystem.FileSystemsModel() + fsd = filesystem.FileSystemModel() + blkdev = '/testfile' + mntpt = '/test' + persistent = False + + with RollbackContext() as rollback: + fs.create({'blk_dev':blkdev, 'mount_point':mntpt, 'persistent':persistent}) + rollback.prependDefer(fsd.delete, mntpt) + + with self.assertRaises(OperationFailed): + fs.create({'blk_dev':blkdev, 'mount_point':mntpt, 'persistent':persistent}) + +