summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cloudinit/config/cc_resizefs.py12
-rw-r--r--cloudinit/tests/test_util.py46
-rw-r--r--cloudinit/util.py23
-rw-r--r--tests/unittests/test_handler/test_handler_resizefs.py22
4 files changed, 95 insertions, 8 deletions
diff --git a/cloudinit/config/cc_resizefs.py b/cloudinit/config/cc_resizefs.py
index 0d282e63..cec22bb7 100644
--- a/cloudinit/config/cc_resizefs.py
+++ b/cloudinit/config/cc_resizefs.py
@@ -59,7 +59,17 @@ __doc__ = get_schema_doc(schema) # Supplement python help()
def _resize_btrfs(mount_point, devpth):
- return ('btrfs', 'filesystem', 'resize', 'max', mount_point)
+ # If "/" is ro resize will fail. However it should be allowed since resize
+ # makes everything bigger and subvolumes that are not ro will benefit.
+ # Use a subvolume that is not ro to trick the resize operation to do the
+ # "right" thing. The use of ".snapshot" is specific to "snapper" a generic
+ # solution would be walk the subvolumes and find a rw mounted subvolume.
+ if (not util.mount_is_read_write(mount_point) and
+ os.path.isdir("%s/.snapshots" % mount_point)):
+ return ('btrfs', 'filesystem', 'resize', 'max',
+ '%s/.snapshots' % mount_point)
+ else:
+ return ('btrfs', 'filesystem', 'resize', 'max', mount_point)
def _resize_ext(mount_point, devpth):
diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py
new file mode 100644
index 00000000..ba6bf699
--- /dev/null
+++ b/cloudinit/tests/test_util.py
@@ -0,0 +1,46 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""Tests for cloudinit.util"""
+
+import logging
+
+import cloudinit.util as util
+
+from cloudinit.tests.helpers import CiTestCase, mock
+
+LOG = logging.getLogger(__name__)
+
+MOUNT_INFO = [
+ '68 0 8:3 / / ro,relatime shared:1 - btrfs /dev/sda1 ro,attr2,inode64',
+ '153 68 254:0 / /home rw,relatime shared:101 - xfs /dev/sda2 rw,attr2'
+]
+
+
+class TestUtil(CiTestCase):
+
+ def test_parse_mount_info_no_opts_no_arg(self):
+ result = util.parse_mount_info('/home', MOUNT_INFO, LOG)
+ self.assertEqual(('/dev/sda2', 'xfs', '/home'), result)
+
+ def test_parse_mount_info_no_opts_arg(self):
+ result = util.parse_mount_info('/home', MOUNT_INFO, LOG, False)
+ self.assertEqual(('/dev/sda2', 'xfs', '/home'), result)
+
+ def test_parse_mount_info_with_opts(self):
+ result = util.parse_mount_info('/', MOUNT_INFO, LOG, True)
+ self.assertEqual(
+ ('/dev/sda1', 'btrfs', '/', 'ro,relatime'),
+ result
+ )
+
+ @mock.patch('cloudinit.util.get_mount_info')
+ def test_mount_is_rw(self, m_mount_info):
+ m_mount_info.return_value = ('/dev/sda1', 'btrfs', '/', 'rw,relatime')
+ is_rw = util.mount_is_read_write('/')
+ self.assertEqual(is_rw, True)
+
+ @mock.patch('cloudinit.util.get_mount_info')
+ def test_mount_is_ro(self, m_mount_info):
+ m_mount_info.return_value = ('/dev/sda1', 'btrfs', '/', 'ro,relatime')
+ is_rw = util.mount_is_read_write('/')
+ self.assertEqual(is_rw, False)
diff --git a/cloudinit/util.py b/cloudinit/util.py
index df0aa5db..9976400f 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -2059,7 +2059,7 @@ def expand_package_list(version_fmt, pkgs):
return pkglist
-def parse_mount_info(path, mountinfo_lines, log=LOG):
+def parse_mount_info(path, mountinfo_lines, log=LOG, get_mnt_opts=False):
"""Return the mount information for PATH given the lines from
/proc/$$/mountinfo."""
@@ -2121,11 +2121,16 @@ def parse_mount_info(path, mountinfo_lines, log=LOG):
match_mount_point = mount_point
match_mount_point_elements = mount_point_elements
+ mount_options = parts[5]
- if devpth and fs_type and match_mount_point:
- return (devpth, fs_type, match_mount_point)
+ if get_mnt_opts:
+ if devpth and fs_type and match_mount_point and mount_options:
+ return (devpth, fs_type, match_mount_point, mount_options)
else:
- return None
+ if devpth and fs_type and match_mount_point:
+ return (devpth, fs_type, match_mount_point)
+
+ return None
def parse_mtab(path):
@@ -2195,7 +2200,7 @@ def parse_mount(path):
return None
-def get_mount_info(path, log=LOG):
+def get_mount_info(path, log=LOG, get_mnt_opts=False):
# Use /proc/$$/mountinfo to find the device where path is mounted.
# This is done because with a btrfs filesystem using os.stat(path)
# does not return the ID of the device.
@@ -2227,7 +2232,7 @@ def get_mount_info(path, log=LOG):
mountinfo_path = '/proc/%s/mountinfo' % os.getpid()
if os.path.exists(mountinfo_path):
lines = load_file(mountinfo_path).splitlines()
- return parse_mount_info(path, lines, log)
+ return parse_mount_info(path, lines, log, get_mnt_opts)
elif os.path.exists("/etc/mtab"):
return parse_mtab(path)
else:
@@ -2613,4 +2618,10 @@ def wait_for_files(flist, maxwait, naplen=.5, log_pre=""):
return need
+def mount_is_read_write(mount_point):
+ """Check whether the given mount point is mounted rw"""
+ result = get_mount_info(mount_point, get_mnt_opts=True)
+ mount_opts = result[-1].split(',')
+ return mount_opts[0] == 'rw'
+
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_handler/test_handler_resizefs.py b/tests/unittests/test_handler/test_handler_resizefs.py
index 29d5574d..5aa3c498 100644
--- a/tests/unittests/test_handler/test_handler_resizefs.py
+++ b/tests/unittests/test_handler/test_handler_resizefs.py
@@ -1,7 +1,7 @@
# This file is part of cloud-init. See LICENSE file for license information.
from cloudinit.config.cc_resizefs import (
- can_skip_resize, handle, maybe_get_writable_device_path)
+ can_skip_resize, handle, maybe_get_writable_device_path, _resize_btrfs)
from collections import namedtuple
import logging
@@ -293,5 +293,25 @@ class TestMaybeGetDevicePathAsWritableBlock(CiTestCase):
" per kernel cmdline",
self.logs.getvalue())
+ @mock.patch('cloudinit.util.mount_is_read_write')
+ @mock.patch('cloudinit.config.cc_resizefs.os.path.isdir')
+ def test_resize_btrfs_mount_is_ro(self, m_is_dir, m_is_rw):
+ """Do not resize / directly if it is read-only. (LP: #1734787)."""
+ m_is_rw.return_value = False
+ m_is_dir.return_value = True
+ self.assertEqual(
+ ('btrfs', 'filesystem', 'resize', 'max', '//.snapshots'),
+ _resize_btrfs("/", "/dev/sda1"))
+
+ @mock.patch('cloudinit.util.mount_is_read_write')
+ @mock.patch('cloudinit.config.cc_resizefs.os.path.isdir')
+ def test_resize_btrfs_mount_is_rw(self, m_is_dir, m_is_rw):
+ """Do not resize / directly if it is read-only. (LP: #1734787)."""
+ m_is_rw.return_value = True
+ m_is_dir.return_value = True
+ self.assertEqual(
+ ('btrfs', 'filesystem', 'resize', 'max', '/'),
+ _resize_btrfs("/", "/dev/sda1"))
+
# vi: ts=4 expandtab