Skip to content

Commit

Permalink
Add filesystem.MountPoint (#74)
Browse files Browse the repository at this point in the history
You can now mount, remount and unmount devices
  • Loading branch information
vkondula authored and lukas-bednar committed Oct 10, 2016
1 parent bb546ea commit 849feff
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 0 deletions.
12 changes: 12 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ There is one special method which allows transfer file between hosts.
h2, "/path/to/file/on/h2/or/target/dir",
)
You can also mount devices.

.. code:: python
with h.fs.mount_point(
'//example.com/share', opts='ro,guest',
fstype='cifs', target='/mnt/netdisk'
) as mp:
h.fs.listdir(mp.target) # list mounted directory
mp.remount('rw,sync,guest') # remount with different options
h.fs.touch('%s/new_file' % mp.target) # touch file
Network
~~~~~~~

Expand Down
45 changes: 45 additions & 0 deletions rrmngmnt/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,48 @@ def __str__(self):
return "Operation '{0}' is not supported for {1}: {2}".format(
self.operation, self.host, self.reason
)


class FileSystemError(GeneralResourceError):
pass


class MountError(FileSystemError):
def __init__(self, mp):
self.mp = mp


class FailCreateTemp(FileSystemError):
pass


class MountCommandError(MountError):
def __init__(self, mp, stdout, stderr):
super(MountCommandError, self).__init__(mp)
self.stdout = stdout
self.stderr = stderr

def __str__(self):
return (
"""
stdout:{out}
stderr:{err}
{mp}
""".format(
out=self.stdout,
err=self.stderr,
mp=self.mp,
)
)


class FailToMount(MountCommandError):
pass


class FailToUmount(MountCommandError):
pass


class FailToRemount(MountCommandError):
pass
143 changes: 143 additions & 0 deletions rrmngmnt/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from rrmngmnt import errors
from rrmngmnt.service import Service
from rrmngmnt.resource import Resource


class FileSystem(Service):
Expand Down Expand Up @@ -244,3 +245,145 @@ def wget(self, url, output_file, progress_handler=None):
"Failed to download file from url {0}".format(url)
)
return output_file

def mktemp(self, template=None, tmpdir=None, directory=False):
"""
Make temporary file
:param template: template for path, 'X's are replaced
:type template: str
:param tmpdir: where to create file, if not specified
use $TMPDIR if set, else /tmp
:type tmpdir: str
:param directory: create directory instead of a file
:type directory: bool
:return: absolute path to file or None if failed
:rtype: str
"""
cmd = ['mktemp']
if tmpdir:
cmd.extend(['-p', tmpdir])
if directory:
cmd.append('-d')
if template:
cmd.append(template)
rc, out, _ = self.host.run_command(cmd)
if rc:
raise errors.FailCreateTemp(cmd)
return out.replace('\n', '')

def mount_point(
self, source, target=None, fs_type=None, opts=None
):
return MountPoint(
self,
source=source,
target=target,
fs_type=fs_type,
opts=opts,
)


class MountPoint(Resource):
"""
Class for mounting devices.
"""
def __init__(self, fs, source, target=None, fs_type=None, opts=None):
"""
Mounts source to target mount point
__author__ = "vkondula"
:param fs: FileSystem object instance
:type fs: FileSystem
:param source: Full path to source
:type source: str
:param target: Path to target directory, if omitted, a temporary
folder is created instead
:type target: str
:param fs_type: File system type
:type fs_type: str
:param opts: Mount options separated by a comma such as:
'sync,rw,guest'
:type opts: str
"""
super(MountPoint, self).__init__()
self.fs = fs
self.source = source
self.opts = opts
self.fs_type = fs_type
self.target = target
self._tmp = not bool(target)
self._mounted = False

def __enter__(self):
self.mount()
return self

def __exit__(self, type_, value, tb):
try:
self.umount()
except errors.MountError as e:
self.logger.error(e)
if not type_:
raise

def __str__(self):
return (
"""
Mounting point:
source: {source}
target: {target}
file system: {fs}
options: {opts}
""".format(
source=self.source,
target=self.target or "*tmp*",
fs=self.fs_type or "DEFAULT",
opts=self.opts or "DEFAULT",
)
)

def mount(self):
if self._tmp:
self.target = self.fs.mktemp(directory=True)
cmd = ['mount', '-v']
if self.fs_type:
cmd.extend(['-t', self.fs_type])
if self.opts:
cmd.extend(['-o', self.opts])
cmd.extend([self.source, self.target])
rc, out, err = self.fs.host.run_command(cmd)
if rc:
raise errors.FailToMount(self, out, err)
self._mounted = True

def umount(self, force=True):
cmd = ['umount', '-v']
if force:
cmd.append('-f')
cmd.append(self.target)
rc, out, err = self.fs.host.run_command(cmd)
if rc:
raise errors.FailToUmount(self, out, err)
if self._tmp and not self.fs.listdir(self.target):
self.fs.rmdir(self.target)
self._mounted = False

def remount(self, opts):
"""
Remount disk
'remount' option is implicit
:param opts: Mount options separated by a comma such as:
'sync,rw,guest'
:type opts: str
"""
if not self._mounted:
raise errors.FailToRemount(self, '', 'not mounted!')
cmd = ['mount', '-v']
cmd.extend(['-o', 'remount,%s' % opts])
cmd.append(self.target)
rc, out, err = self.fs.host.run_command(cmd)
if rc:
raise errors.FailToRemount(self, out, err)
self.opts = opts
20 changes: 20 additions & 0 deletions tests/test_filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ class TestFilesystem(object):
'touch /path/to/nopermission': (1, '', ''),
'ls -A1 /path/to/empty': (0, '\n', ''),
'ls -A1 /path/to/two': (0, 'first\nsecond\n', ''),
'mktemp -d': (0, '/path/to/tmpdir', ''),
'mount -v -t xfs -o bind,ro /path/to /path/to/tmpdir': (0, '', ''),
'ls -A1 /path/to/tmpdir': (0, 'first\nsecond\n', ''),
'mount -v -o remount,bind,rw /path/to/tmpdir': (0, '', ''),
'umount -v -f /path/to/tmpdir': (0, '', ''),
'mount -v /not/device /path/to/tmpdir': (
32, '', '/not/device is not a block device\n'
),
}
files = {}

Expand Down Expand Up @@ -138,6 +146,18 @@ def test_listdir_two(self):
'first', 'second',
]

def test_mount_point(self):
with self.get_host().fs.mount_point(
'/path/to', opts='bind,ro', fs_type='xfs'
) as mp:
assert not mp.remount('bind,rw')

def test_fail_mount(self):
with pytest.raises(errors.MountError) as ex_info:
with self.get_host().fs.mount_point('/not/device'):
pass
assert "is not a block device" in str(ex_info.value)


class TestFSGetPutFile(object):
data = {
Expand Down

0 comments on commit 849feff

Please sign in to comment.