d1c5ca
From 17f972b6fb172fe19d6e115a20664eefdbd3838d Mon Sep 17 00:00:00 2001
d1c5ca
From: Eduardo Otubo <otubo@redhat.com>
d1c5ca
Date: Mon, 24 Aug 2020 15:25:38 +0200
d1c5ca
Subject: [PATCH 3/3] Detect kernel version before swap file creation (#428)
d1c5ca
d1c5ca
RH-Author: Eduardo Otubo <otubo@redhat.com>
d1c5ca
Message-id: <20200820092042.5418-4-otubo@redhat.com>
d1c5ca
Patchwork-id: 98191
d1c5ca
O-Subject: [RHEL-8.3.0 cloud-init PATCH 3/3] Detect kernel version before swap file creation (#428)
d1c5ca
Bugzilla: 1794664
d1c5ca
RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com>
d1c5ca
RH-Acked-by: Mohammed Gamal <mgamal@redhat.com>
d1c5ca
d1c5ca
commit b749548a9eb43b34cce64f8688107645411abc8c
d1c5ca
Author: Eduardo Otubo <otubo@redhat.com>
d1c5ca
Date:   Tue Aug 18 23:12:02 2020 +0200
d1c5ca
d1c5ca
    Detect kernel version before swap file creation (#428)
d1c5ca
d1c5ca
    According to man page `man 8 swapon', "Preallocated swap files are
d1c5ca
    supported on XFS since Linux 4.18". This patch checks for kernel version
d1c5ca
    before attepting to create swapfile, using dd for XFS only on kernel
d1c5ca
    versions <= 4.18 or btrfs.
d1c5ca
d1c5ca
    Add new func util.kernel_version which returns a tuple of ints (major, minor)
d1c5ca
d1c5ca
    Signed-off-by: Eduardo Otubo otubo@redhat.com
d1c5ca
d1c5ca
Signed-off-by: Eduardo Otubo otubo@redhat.com
d1c5ca
Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
d1c5ca
---
d1c5ca
 cloudinit/config/cc_mounts.py                      |   8 +-
d1c5ca
 cloudinit/util.py                                  |   4 +
d1c5ca
 .../unittests/test_handler/test_handler_mounts.py  | 107 +++++++++++++++++++++
d1c5ca
 tests/unittests/test_util.py                       |  15 +++
d1c5ca
 4 files changed, 131 insertions(+), 3 deletions(-)
d1c5ca
d1c5ca
diff --git a/cloudinit/config/cc_mounts.py b/cloudinit/config/cc_mounts.py
d1c5ca
index 0573026..e1c43e3 100644
d1c5ca
--- a/cloudinit/config/cc_mounts.py
d1c5ca
+++ b/cloudinit/config/cc_mounts.py
d1c5ca
@@ -65,7 +65,7 @@ swap file is created.
d1c5ca
 from string import whitespace
d1c5ca
 
d1c5ca
 import logging
d1c5ca
-import os.path
d1c5ca
+import os
d1c5ca
 import re
d1c5ca
 
d1c5ca
 from cloudinit import type_utils
d1c5ca
@@ -249,7 +249,8 @@ def create_swapfile(fname, size):
d1c5ca
 
d1c5ca
     fstype = util.get_mount_info(swap_dir)[1]
d1c5ca
 
d1c5ca
-    if fstype in ("xfs", "btrfs"):
d1c5ca
+    if (fstype == "xfs" and
d1c5ca
+            util.kernel_version() < (4, 18)) or fstype == "btrfs":
d1c5ca
         create_swap(fname, size, "dd")
d1c5ca
     else:
d1c5ca
         try:
d1c5ca
@@ -259,7 +260,8 @@ def create_swapfile(fname, size):
d1c5ca
             LOG.warning("Will attempt with dd.")
d1c5ca
             create_swap(fname, size, "dd")
d1c5ca
 
d1c5ca
-    util.chmod(fname, 0o600)
d1c5ca
+    if os.path.exists(fname):
d1c5ca
+        util.chmod(fname, 0o600)
d1c5ca
     try:
d1c5ca
         util.subp(['mkswap', fname])
d1c5ca
     except util.ProcessExecutionError:
d1c5ca
diff --git a/cloudinit/util.py b/cloudinit/util.py
d1c5ca
index 5d51ba8..ad89376 100644
d1c5ca
--- a/cloudinit/util.py
d1c5ca
+++ b/cloudinit/util.py
d1c5ca
@@ -79,6 +79,10 @@ CONTAINER_TESTS = (['systemd-detect-virt', '--quiet', '--container'],
d1c5ca
                    ['lxc-is-container'])
d1c5ca
 
d1c5ca
 
d1c5ca
+def kernel_version():
d1c5ca
+    return tuple(map(int, os.uname().release.split('.')[:2]))
d1c5ca
+
d1c5ca
+
d1c5ca
 @lru_cache()
d1c5ca
 def get_architecture(target=None):
d1c5ca
     out, _ = subp(['dpkg', '--print-architecture'], capture=True,
d1c5ca
diff --git a/tests/unittests/test_handler/test_handler_mounts.py b/tests/unittests/test_handler/test_handler_mounts.py
d1c5ca
index 7bcefa0..27bcc6f 100644
d1c5ca
--- a/tests/unittests/test_handler/test_handler_mounts.py
d1c5ca
+++ b/tests/unittests/test_handler/test_handler_mounts.py
d1c5ca
@@ -132,6 +132,113 @@ class TestSanitizeDevname(test_helpers.FilesystemMockingTestCase):
d1c5ca
                 'ephemeral0.1', lambda x: disk_path, mock.Mock()))
d1c5ca
 
d1c5ca
 
d1c5ca
+class TestSwapFileCreation(test_helpers.FilesystemMockingTestCase):
d1c5ca
+
d1c5ca
+    def setUp(self):
d1c5ca
+        super(TestSwapFileCreation, self).setUp()
d1c5ca
+        self.new_root = self.tmp_dir()
d1c5ca
+        self.patchOS(self.new_root)
d1c5ca
+
d1c5ca
+        self.fstab_path = os.path.join(self.new_root, 'etc/fstab')
d1c5ca
+        self.swap_path = os.path.join(self.new_root, 'swap.img')
d1c5ca
+        self._makedirs('/etc')
d1c5ca
+
d1c5ca
+        self.add_patch('cloudinit.config.cc_mounts.FSTAB_PATH',
d1c5ca
+                       'mock_fstab_path',
d1c5ca
+                       self.fstab_path,
d1c5ca
+                       autospec=False)
d1c5ca
+
d1c5ca
+        self.add_patch('cloudinit.config.cc_mounts.subp.subp',
d1c5ca
+                       'm_subp_subp')
d1c5ca
+
d1c5ca
+        self.add_patch('cloudinit.config.cc_mounts.util.mounts',
d1c5ca
+                       'mock_util_mounts',
d1c5ca
+                       return_value={
d1c5ca
+                           '/dev/sda1': {'fstype': 'ext4',
d1c5ca
+                                         'mountpoint': '/',
d1c5ca
+                                         'opts': 'rw,relatime,discard'
d1c5ca
+                                         }})
d1c5ca
+
d1c5ca
+        self.mock_cloud = mock.Mock()
d1c5ca
+        self.mock_log = mock.Mock()
d1c5ca
+        self.mock_cloud.device_name_to_device = self.device_name_to_device
d1c5ca
+
d1c5ca
+        self.cc = {
d1c5ca
+            'swap': {
d1c5ca
+                'filename': self.swap_path,
d1c5ca
+                'size': '512',
d1c5ca
+                'maxsize': '512'}}
d1c5ca
+
d1c5ca
+    def _makedirs(self, directory):
d1c5ca
+        directory = os.path.join(self.new_root, directory.lstrip('/'))
d1c5ca
+        if not os.path.exists(directory):
d1c5ca
+            os.makedirs(directory)
d1c5ca
+
d1c5ca
+    def device_name_to_device(self, path):
d1c5ca
+        if path == 'swap':
d1c5ca
+            return self.swap_path
d1c5ca
+        else:
d1c5ca
+            dev = None
d1c5ca
+
d1c5ca
+        return dev
d1c5ca
+
d1c5ca
+    @mock.patch('cloudinit.util.get_mount_info')
d1c5ca
+    @mock.patch('cloudinit.util.kernel_version')
d1c5ca
+    def test_swap_creation_method_fallocate_on_xfs(self, m_kernel_version,
d1c5ca
+                                                   m_get_mount_info):
d1c5ca
+        m_kernel_version.return_value = (4, 20)
d1c5ca
+        m_get_mount_info.return_value = ["", "xfs"]
d1c5ca
+
d1c5ca
+        cc_mounts.handle(None, self.cc, self.mock_cloud, self.mock_log, [])
d1c5ca
+        self.m_subp_subp.assert_has_calls([
d1c5ca
+            mock.call(['fallocate', '-l', '0M', self.swap_path], capture=True),
d1c5ca
+            mock.call(['mkswap', self.swap_path]),
d1c5ca
+            mock.call(['swapon', '-a'])])
d1c5ca
+
d1c5ca
+    @mock.patch('cloudinit.util.get_mount_info')
d1c5ca
+    @mock.patch('cloudinit.util.kernel_version')
d1c5ca
+    def test_swap_creation_method_xfs(self, m_kernel_version,
d1c5ca
+                                      m_get_mount_info):
d1c5ca
+        m_kernel_version.return_value = (3, 18)
d1c5ca
+        m_get_mount_info.return_value = ["", "xfs"]
d1c5ca
+
d1c5ca
+        cc_mounts.handle(None, self.cc, self.mock_cloud, self.mock_log, [])
d1c5ca
+        self.m_subp_subp.assert_has_calls([
d1c5ca
+            mock.call(['dd', 'if=/dev/zero',
d1c5ca
+                       'of=' + self.swap_path,
d1c5ca
+                       'bs=1M', 'count=0'], capture=True),
d1c5ca
+            mock.call(['mkswap', self.swap_path]),
d1c5ca
+            mock.call(['swapon', '-a'])])
d1c5ca
+
d1c5ca
+    @mock.patch('cloudinit.util.get_mount_info')
d1c5ca
+    @mock.patch('cloudinit.util.kernel_version')
d1c5ca
+    def test_swap_creation_method_btrfs(self, m_kernel_version,
d1c5ca
+                                        m_get_mount_info):
d1c5ca
+        m_kernel_version.return_value = (4, 20)
d1c5ca
+        m_get_mount_info.return_value = ["", "btrfs"]
d1c5ca
+
d1c5ca
+        cc_mounts.handle(None, self.cc, self.mock_cloud, self.mock_log, [])
d1c5ca
+        self.m_subp_subp.assert_has_calls([
d1c5ca
+            mock.call(['dd', 'if=/dev/zero',
d1c5ca
+                       'of=' + self.swap_path,
d1c5ca
+                       'bs=1M', 'count=0'], capture=True),
d1c5ca
+            mock.call(['mkswap', self.swap_path]),
d1c5ca
+            mock.call(['swapon', '-a'])])
d1c5ca
+
d1c5ca
+    @mock.patch('cloudinit.util.get_mount_info')
d1c5ca
+    @mock.patch('cloudinit.util.kernel_version')
d1c5ca
+    def test_swap_creation_method_ext4(self, m_kernel_version,
d1c5ca
+                                       m_get_mount_info):
d1c5ca
+        m_kernel_version.return_value = (5, 14)
d1c5ca
+        m_get_mount_info.return_value = ["", "ext4"]
d1c5ca
+
d1c5ca
+        cc_mounts.handle(None, self.cc, self.mock_cloud, self.mock_log, [])
d1c5ca
+        self.m_subp_subp.assert_has_calls([
d1c5ca
+            mock.call(['fallocate', '-l', '0M', self.swap_path], capture=True),
d1c5ca
+            mock.call(['mkswap', self.swap_path]),
d1c5ca
+            mock.call(['swapon', '-a'])])
d1c5ca
+
d1c5ca
+
d1c5ca
 class TestFstabHandling(test_helpers.FilesystemMockingTestCase):
d1c5ca
 
d1c5ca
     swap_path = '/dev/sdb1'
d1c5ca
diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py
d1c5ca
index 0e71db8..87dc8dd 100644
d1c5ca
--- a/tests/unittests/test_util.py
d1c5ca
+++ b/tests/unittests/test_util.py
d1c5ca
@@ -1177,4 +1177,19 @@ class TestGetProcEnv(helpers.TestCase):
d1c5ca
         my_ppid = os.getppid()
d1c5ca
         self.assertEqual(my_ppid, util.get_proc_ppid(my_pid))
d1c5ca
 
d1c5ca
+
d1c5ca
+class TestKernelVersion():
d1c5ca
+    """test kernel version function"""
d1c5ca
+
d1c5ca
+    params = [
d1c5ca
+        ('5.6.19-300.fc32.x86_64', (5, 6)),
d1c5ca
+        ('4.15.0-101-generic', (4, 15)),
d1c5ca
+        ('3.10.0-1062.12.1.vz7.131.10', (3, 10)),
d1c5ca
+        ('4.18.0-144.el8.x86_64', (4, 18))]
d1c5ca
+
d1c5ca
+    @mock.patch('os.uname')
d1c5ca
+    @pytest.mark.parametrize("uname_release,expected", params)
d1c5ca
+    def test_kernel_version(self, m_uname, uname_release, expected):
d1c5ca
+        m_uname.return_value.release = uname_release
d1c5ca
+        assert expected == util.kernel_version()
d1c5ca
 # vi: ts=4 expandtab
d1c5ca
-- 
d1c5ca
1.8.3.1
d1c5ca