Import mounted context manager from IPA

This pattern is used in a few places and is very helpful for
writing custom deploy steps.

Change-Id: Ib7518075f3140a8daf06db52317ff10f8572dcc8
This commit is contained in:
Dmitry Tantsur 2021-01-09 17:46:35 +01:00
parent 0bbba8e62f
commit ebdf6784cb
3 changed files with 119 additions and 0 deletions

View File

@ -21,6 +21,7 @@ partprobe: CommandFilter, partprobe, root
mkswap: CommandFilter, mkswap, root mkswap: CommandFilter, mkswap, root
mkfs: CommandFilter, mkfs, root mkfs: CommandFilter, mkfs, root
dd: CommandFilter, dd, root dd: CommandFilter, dd, root
mount: CommandFilter, mount, root
# ironic_lib/disk_partitioner.py # ironic_lib/disk_partitioner.py
fuser: CommandFilter, fuser, root fuser: CommandFilter, fuser, root

View File

@ -668,3 +668,68 @@ class GetRouteSourceTestCase(base.IronicLibTestCase):
source = utils.get_route_source('XXX') source = utils.get_route_source('XXX')
self.assertIsNone(source) self.assertIsNone(source)
@mock.patch('shutil.rmtree', autospec=True)
@mock.patch.object(utils, 'execute', autospec=True)
@mock.patch('tempfile.mkdtemp', autospec=True)
class MountedTestCase(base.IronicLibTestCase):
def test_temporary(self, mock_temp, mock_execute, mock_rmtree):
with utils.mounted('/dev/fake') as path:
self.assertIs(path, mock_temp.return_value)
mock_execute.assert_has_calls([
mock.call("mount", '/dev/fake', mock_temp.return_value,
run_as_root=True),
mock.call("umount", mock_temp.return_value, run_as_root=True),
])
mock_rmtree.assert_called_once_with(mock_temp.return_value)
def test_with_dest(self, mock_temp, mock_execute, mock_rmtree):
with utils.mounted('/dev/fake', '/mnt/fake') as path:
self.assertEqual('/mnt/fake', path)
mock_execute.assert_has_calls([
mock.call("mount", '/dev/fake', '/mnt/fake', run_as_root=True),
mock.call("umount", '/mnt/fake', run_as_root=True),
])
self.assertFalse(mock_temp.called)
self.assertFalse(mock_rmtree.called)
def test_with_opts(self, mock_temp, mock_execute, mock_rmtree):
with utils.mounted('/dev/fake', '/mnt/fake',
opts=['ro', 'foo=bar']) as path:
self.assertEqual('/mnt/fake', path)
mock_execute.assert_has_calls([
mock.call("mount", '/dev/fake', '/mnt/fake', '-o', 'ro,foo=bar',
run_as_root=True),
mock.call("umount", '/mnt/fake', run_as_root=True),
])
def test_with_type(self, mock_temp, mock_execute, mock_rmtree):
with utils.mounted('/dev/fake', '/mnt/fake',
fs_type='iso9660') as path:
self.assertEqual('/mnt/fake', path)
mock_execute.assert_has_calls([
mock.call("mount", '/dev/fake', '/mnt/fake', '-t', 'iso9660',
run_as_root=True),
mock.call("umount", '/mnt/fake', run_as_root=True),
])
def test_failed_to_mount(self, mock_temp, mock_execute, mock_rmtree):
mock_execute.side_effect = OSError
self.assertRaises(OSError, utils.mounted('/dev/fake').__enter__)
mock_execute.assert_called_once_with("mount", '/dev/fake',
mock_temp.return_value,
run_as_root=True)
mock_rmtree.assert_called_once_with(mock_temp.return_value)
def test_failed_to_unmount(self, mock_temp, mock_execute, mock_rmtree):
mock_execute.side_effect = [('', ''),
processutils.ProcessExecutionError]
with utils.mounted('/dev/fake', '/mnt/fake') as path:
self.assertEqual('/mnt/fake', path)
mock_execute.assert_has_calls([
mock.call("mount", '/dev/fake', '/mnt/fake', run_as_root=True),
mock.call("umount", '/mnt/fake', run_as_root=True),
])
self.assertFalse(mock_rmtree.called)

View File

@ -18,12 +18,15 @@
"""Utilities and helper functions.""" """Utilities and helper functions."""
import contextlib
import copy import copy
import errno import errno
import ipaddress import ipaddress
import logging import logging
import os import os
import re import re
import shutil
import tempfile
from urllib import parse as urlparse from urllib import parse as urlparse
from oslo_concurrency import processutils from oslo_concurrency import processutils
@ -585,3 +588,53 @@ def get_route_source(dest, ignore_link_local=True):
except (IndexError, ValueError): except (IndexError, ValueError):
LOG.debug('No route to host %(dest)s, route record: %(rec)s', LOG.debug('No route to host %(dest)s, route record: %(rec)s',
{'dest': dest, 'rec': out}) {'dest': dest, 'rec': out})
@contextlib.contextmanager
def mounted(source, dest=None, opts=None, fs_type=None):
"""A context manager for a temporary mount.
:param source: A device to mount.
:param dest: Mount destination. If not specified, a temporary directory
will be created and removed afterwards. An existing destination is
not removed.
:param opts: Mount options (``-o`` argument).
:param fs_type: File system type (``-t`` argument).
:returns: A generator yielding the destination.
"""
params = []
if opts:
params.extend(['-o', ','.join(opts)])
if fs_type:
params.extend(['-t', fs_type])
if dest is None:
dest = tempfile.mkdtemp()
clean_up = True
else:
clean_up = False
mounted = False
try:
execute("mount", source, dest, *params, run_as_root=True)
mounted = True
yield dest
finally:
if mounted:
try:
execute("umount", dest, run_as_root=True)
except (EnvironmentError,
processutils.ProcessExecutionError) as exc:
LOG.warning(
'Unable to unmount temporary location %(dest)s: %(err)s',
{'dest': dest, 'err': exc})
# NOTE(dtantsur): don't try to remove a still mounted location
clean_up = False
if clean_up:
try:
shutil.rmtree(dest)
except EnvironmentError as exc:
LOG.warning(
'Unable to remove temporary location %(dest)s: %(err)s',
{'dest': dest, 'err': exc})