← Back to team overview

curtin-dev team mailing list archive

[Merge] ~dbungert/curtin:compat into curtin:master

 

Dan Bungert has proposed merging ~dbungert/curtin:compat into curtin:master.

Commit message:
add compat module, and use for sfdisk/util-linux quirks


Requested reviews:
  Michael Hudson-Doyle (mwhudson)
  Server Team CI bot (server-team-bot): continuous-integration

For more details, see:
https://code.launchpad.net/~dbungert/curtin/+git/curtin/+merge/443511
-- 
Your team curtin developers is subscribed to branch curtin:master.
diff --git a/curtin/block/__init__.py b/curtin/block/__init__.py
index 2f69bd3..86251d9 100644
--- a/curtin/block/__init__.py
+++ b/curtin/block/__init__.py
@@ -1042,9 +1042,21 @@ def check_dos_signature(device):
     # this signature must be at 0x1fe
     # https://en.wikipedia.org/wiki/Master_boot_record#Sector_layout
     devname = dev_path(path_to_kname(device))
-    return (is_block_device(devname) and util.file_size(devname) >= 0x200 and
-            (util.load_file(devname, decode=False, read_len=2, offset=0x1fe) ==
-             b'\x55\xAA'))
+    if not is_block_device(devname):
+        return False
+    try:
+        # Some older series have the extended partition block device but return
+        # ENXIO when attempting to read it.
+        file_size = util.file_size(devname)
+    except OSError as ose:
+        if ose.errno == errno.ENXIO:
+            return False
+        else:
+            raise
+    if file_size < 0x200:
+        return False
+    signature = util.load_file(devname, decode=False, read_len=2, offset=0x1fe)
+    return signature == b'\x55\xAA'
 
 
 def check_efi_signature(device):
diff --git a/curtin/commands/block_meta.py b/curtin/commands/block_meta.py
index 5c5a7b0..6ce8440 100644
--- a/curtin/commands/block_meta.py
+++ b/curtin/commands/block_meta.py
@@ -1,7 +1,7 @@
 # This file is part of curtin. See LICENSE file for copyright and license info.
 
 from collections import OrderedDict, namedtuple
-from curtin import (block, config, paths, util)
+from curtin import (block, compat, config, paths, util)
 from curtin.block import schemas
 from curtin.block import (bcache, clear_holders, dasd, iscsi, lvm, mdadm, mkfs,
                           multipath, zfs)
@@ -653,7 +653,6 @@ DEVS = set()
 def image_handler(info, storage_config, context):
     path = info['path']
     size = int(util.human2bytes(info['size']))
-    sector_size = str(int(util.human2bytes(info.get('sector_size', 512))))
     if info.get('preserve', False):
         actual_size = os.stat(path).st_size
         if size != actual_size:
@@ -670,10 +669,14 @@ def image_handler(info, storage_config, context):
             if os.path.exists(path):
                 os.unlink(path)
             raise
+
+    cmd = ['losetup', '--show', '--find', path]
+    sector_size = int(util.human2bytes(info.get('sector_size', 512)))
+    if sector_size != 512:
+        compat.supports_large_sectors(fatal=True)
+        cmd.extend(('--sector-size', str(sector_size)))
     try:
-        dev = util.subp([
-            'losetup', '--show', '--sector-size', sector_size, '--find', path],
-            capture=True)[0].strip()
+        dev = util.subp(cmd, capture=True)[0].strip()
     except BaseException:
         if os.path.exists(path) and not info.get('preserve'):
             os.unlink(path)
@@ -858,17 +861,28 @@ def calc_dm_partition_info(partition_kname):
 
 
 def calc_partition_info(partition_kname, logical_block_size_bytes):
+    p_size_sec = 0
+    p_start_sec = 0
     if partition_kname.startswith('dm-'):
         p_start, p_size = calc_dm_partition_info(partition_kname)
     else:
         pdir = block.sys_block_path(partition_kname)
         p_size = int(util.load_file(os.path.join(pdir, "size")))
         p_start = int(util.load_file(os.path.join(pdir, "start")))
+        if not all([p_size, p_start]):
+            # if sysfs reported a 0, let's try sfdisk
+            sfdisk_info = block.sfdisk_info(partition_kname)
+            part_path = block.kname_to_path(partition_kname)
+            part_info = block.get_partition_sfdisk_info(part_path, sfdisk_info)
+            p_size_sec = part_info['size']
+            p_start_sec = part_info['start']
 
     # NB: sys/block/X/{size,start} and dmsetup output are both always
     # in 512b sectors
-    p_size_sec = p_size * 512 // logical_block_size_bytes
-    p_start_sec = p_start * 512 // logical_block_size_bytes
+    if p_size_sec == 0:
+        p_size_sec = p_size * 512 // logical_block_size_bytes
+    if p_start_sec == 0:
+        p_start_sec = p_start * 512 // logical_block_size_bytes
 
     LOG.debug("calc_partition_info: %s size_sectors=%s start_sectors=%s",
               partition_kname, p_size_sec, p_start_sec)
diff --git a/curtin/commands/block_meta_v2.py b/curtin/commands/block_meta_v2.py
index 7f5ebce..570da83 100644
--- a/curtin/commands/block_meta_v2.py
+++ b/curtin/commands/block_meta_v2.py
@@ -8,7 +8,7 @@ from typing import (
 
 import attr
 
-from curtin import (block, util)
+from curtin import (block, compat, util)
 from curtin.commands.block_meta import (
     _get_volume_fstype,
     disk_handler as disk_handler_v1,
@@ -171,9 +171,10 @@ class SFDiskPartTable:
     def apply(self, device):
         sfdisk_script = self.render()
         LOG.debug("sfdisk input:\n---\n%s\n---\n", sfdisk_script)
-        util.subp(
-            ['sfdisk', '--no-tell-kernel', '--no-reread', device],
-            data=sfdisk_script.encode('ascii'))
+        cmd = ['sfdisk', '--no-reread', device]
+        if compat.supports_sfdisk_no_tell_kernel():
+            cmd.append('--no-tell-kernel')
+        util.subp(cmd, data=sfdisk_script.encode('ascii'))
         util.subp(['partprobe', device])
         # sfdisk and partprobe (as invoked here) use ioctls to inform the
         # kernel that the partition table has changed so it can add and remove
diff --git a/curtin/compat.py b/curtin/compat.py
new file mode 100644
index 0000000..c20b650
--- /dev/null
+++ b/curtin/compat.py
@@ -0,0 +1,38 @@
+# This file is part of curtin. See LICENSE file for copyright and license info.
+
+import re
+
+from curtin import util
+
+
+def _get_util_linux_ver():
+    line = util.subp(['losetup', '--version'], capture=True)[0].strip()
+    m = re.fullmatch(r'losetup from util-linux ([\d.]+)', line)
+    if m is None:
+        return None
+    return m.group(1)
+
+
+def _check_util_linux_ver(ver, label='', fatal=False):
+    ul_ver = _get_util_linux_ver()
+    result = ul_ver is not None and ul_ver >= ver
+    if not result and fatal:
+        raise RuntimeError(
+            'this system lacks the required {} support'.format(label))
+    return result
+
+
+def supports_large_sectors(fatal=False):
+    # Known requirements:
+    # * Kernel 4.14+
+    #   * Minimum supported things have a higher kernel, so skip that check
+    # * util-linux 2.30+
+    #   * xenial has 2.27.1, bionic has 2.31.1
+    # However, see also this, which suggests using 2.37.1:
+    # https://lore.kernel.org/lkml/20210615084259.yj5pmyjonfqcg7lg@xxxxxxxxxxx/
+    return _check_util_linux_ver('2.37.1', 'large sector', fatal)
+
+
+def supports_sfdisk_no_tell_kernel(fatal=False):
+    # Needs util-linux 2.29+
+    return _check_util_linux_ver('2.29', 'sfdisk', fatal)
diff --git a/requirements.txt b/requirements.txt
index d8d1a3d..71a2637 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,6 @@
+pip
+wheel
+virtualenv
 pyudev
 pyyaml
 oauthlib
diff --git a/tests/integration/test_block_meta.py b/tests/integration/test_block_meta.py
index 0afa9fd..e571cdc 100644
--- a/tests/integration/test_block_meta.py
+++ b/tests/integration/test_block_meta.py
@@ -8,12 +8,17 @@ import os
 from parameterized import parameterized
 import re
 import sys
+import tempfile
 from typing import Optional
 from unittest import skipIf
 import yaml
 
+<<<<<<< tests/integration/test_block_meta.py
 from curtin import block, distro, log, udev, util
 
+=======
+from curtin import block, compat, distro, log, udev, util
+>>>>>>> tests/integration/test_block_meta.py
 from curtin.commands.block_meta import _get_volume_fstype
 from curtin.commands.block_meta_v2 import ONE_MIB_BYTES
 
@@ -27,11 +32,10 @@ class IntegrationTestCase(CiTestCase):
 
 @contextlib.contextmanager
 def loop_dev(image, sector_size=512):
-    dev = util.subp([
-        'losetup',
-        '--show', '--find', '--sector-size', str(sector_size),
-        image,
-        ], capture=True, decode='ignore')[0].strip()
+    cmd = ['losetup', '--show', '--find', image]
+    if sector_size != 512:
+        cmd.extend(('--sector-size', str(sector_size)))
+    dev = util.subp(cmd, capture=True, decode='ignore')[0].strip()
     util.subp(['partprobe', dev])
     try:
         udev.udevadm_trigger([dev])
@@ -118,7 +122,7 @@ def summarize_partitions(dev):
     parts = []
     ptable_json = util.subp(['sfdisk', '-J', dev], capture=True)[0]
     ptable = json.loads(ptable_json)['partitiontable']
-    sectorsize = ptable['sectorsize']
+    sectorsize = ptable.get('sectorsize', 512)
     assert dev == ptable['device']
     sysfs_data = block.sysfs_partition_data(dev)
     for part in ptable['partitions']:
@@ -257,7 +261,25 @@ class TestBlockMeta(IntegrationTestCase):
             '-c', config_path, 'block-meta', '--testmode', 'custom',
             *args,
             ]
-        util.subp(cmd, env=cmd_env, **kwargs)
+
+        # Set debug=True to halt the integration test and run curtin manually,
+        # with the integration tests having setup the environment for you.
+        # Run tests with pytest-3 -s option.
+        if kwargs.pop('debug', False):
+            with tempfile.NamedTemporaryFile(mode='w', delete=False) as fp:
+                fp.write('#!/bin/bash\n')
+                for k, v in cmd_env.items():
+                    fp.write('export {}="{}"\n'.format(k, v))
+                fp.write('export PYTHONPATH="{}"\n'.format(os.getcwd()))
+                fp.write(' '.join(cmd) + '\n')
+            os.chmod(fp.name, 0o700)
+            print()
+            print('The integration test is paused.')
+            print('Use script {} to run curtin manually.'.format(fp.name))
+            breakpoint()
+            os.unlink(fp.name)
+        else:
+            util.subp(cmd, env=cmd_env, **kwargs)
 
     def _test_default_offsets(self, ptable, version, sector_size=512):
         psize = 40 << 20
@@ -311,15 +333,19 @@ class TestBlockMeta(IntegrationTestCase):
     def test_default_offsets_msdos_v2(self):
         self._test_default_offsets('msdos', 2)
 
+    @skipIf(not compat.supports_large_sectors(), 'test is for large sectors')
     def test_default_offsets_gpt_v1_4k(self):
         self._test_default_offsets('gpt', 1, 4096)
 
+    @skipIf(not compat.supports_large_sectors(), 'test is for large sectors')
     def test_default_offsets_msdos_v1_4k(self):
         self._test_default_offsets('msdos', 1, 4096)
 
+    @skipIf(not compat.supports_large_sectors(), 'test is for large sectors')
     def test_default_offsets_gpt_v2_4k(self):
         self._test_default_offsets('gpt', 2, 4096)
 
+    @skipIf(not compat.supports_large_sectors(), 'test is for large sectors')
     def test_default_offsets_msdos_v2_4k(self):
         self._test_default_offsets('msdos', 2, 4096)
 
@@ -424,8 +450,17 @@ class TestBlockMeta(IntegrationTestCase):
                     PartData(number=6, offset=13 << 20, size=10 << 20),
                 ])
 
-            p1kname = block.partition_kname(block.path_to_kname(dev), 1)
-            self.assertTrue(block.is_extended_partition('/dev/' + p1kname))
+            if distro.lsb_release()['release'] >= '20.04':
+                p1kname = block.partition_kname(block.path_to_kname(dev), 1)
+                self.assertTrue(block.is_extended_partition('/dev/' + p1kname))
+            else:
+                # on Bionic and earlier, the block device for the extended
+                # partition is not functional, so attempting to verify it is
+                # expected to fail.  So just read the value directly from the
+                # expected signature location.
+                signature = util.load_file(dev, decode=False,
+                                           read_len=2, offset=0x1001fe)
+                self.assertEqual(b'\x55\xAA', signature)
 
     def test_logical_v1(self):
         self._test_logical(1)
@@ -716,6 +751,8 @@ class TestBlockMeta(IntegrationTestCase):
                     PartData(number=6, offset=13 << 20, size=20 << 20),
                 ])
 
+    @skipIf(distro.lsb_release()['release'] < '20.04',
+            'old lsblk will not list info about extended partitions')
     def test_resize_extended(self):
         img = self.tmp_path('image.img')
         config = StorageConfigBuilder(version=2)
@@ -865,6 +902,8 @@ class TestBlockMeta(IntegrationTestCase):
             self.assertEqual(139 << 20, _get_filesystem_size(dev, p2))
             self.assertEqual(50 << 20, _get_filesystem_size(dev, p3))
 
+    @skipIf(distro.lsb_release()['release'] < '20.04',
+            'old lsblk will not list info about extended partitions')
     def test_mix_of_operations_msdos(self):
         # a test that keeps, creates, resizes, and deletes a partition
         # including handling of extended/logical
diff --git a/tests/unittests/test_compat.py b/tests/unittests/test_compat.py
new file mode 100644
index 0000000..0ba5002
--- /dev/null
+++ b/tests/unittests/test_compat.py
@@ -0,0 +1,34 @@
+# This file is part of curtin. See LICENSE file for copyright and license info.
+
+from unittest import mock
+
+from curtin import compat
+from .helpers import CiTestCase
+
+
+class TestUtilLinuxVer(CiTestCase):
+    @mock.patch('curtin.util.subp')
+    def test_ul_ver(self, m_subp):
+        m_subp.return_value = ('losetup from util-linux 2.31.1', '')
+        self.assertEqual('2.31.1', compat._get_util_linux_ver())
+
+    @mock.patch('curtin.util.subp')
+    def test_ul_malformed(self, m_subp):
+        m_subp.return_value = ('losetup from util-linux asdf', '')
+        self.assertEqual(None, compat._get_util_linux_ver())
+
+    @mock.patch('curtin.compat._get_util_linux_ver')
+    def test_verpass(self, m_gulv):
+        m_gulv.return_value = '1.23.4'
+        self.assertTrue(compat._check_util_linux_ver('1.20'))
+
+    @mock.patch('curtin.compat._get_util_linux_ver')
+    def test_verfail(self, m_gulv):
+        m_gulv.return_value = '1.23.4'
+        self.assertFalse(compat._check_util_linux_ver('1.24'))
+
+    @mock.patch('curtin.compat._get_util_linux_ver')
+    def test_verfatal(self, m_gulv):
+        m_gulv.return_value = '1.23.4'
+        with self.assertRaisesRegex(RuntimeError, '.*my feature.*'):
+            compat._check_util_linux_ver('1.24', 'my feature', fatal=True)

Follow ups