curtin-dev team mailing list archive
-
curtin-dev team
-
Mailing list archive
-
Message #02897
[Merge] ~dbungert/curtin:series-fixes into curtin:release/23.1
Dan Bungert has proposed merging ~dbungert/curtin:series-fixes into curtin:release/23.1.
Commit message:
Merge fixes to 23.1 release branch
git cherry-pick -x 33411a7..07ec70c3
git cherry-pick -x 9aed2e3^..f0556cc
git cherry-pick -x 0131c2dd^..3969bca
git cherry-pick -x fb70e570..a9d2efd3
git cherry-pick -x 1f7e8591
git cherry-pick -x d7cbc39e^..0fbdf389
Requested reviews:
curtin developers (curtin-dev)
For more details, see:
https://code.launchpad.net/~dbungert/curtin/+git/curtin/+merge/443612
--
Your team curtin developers is requested to review the proposed merge of ~dbungert/curtin:series-fixes into curtin:release/23.1.
diff --git a/Makefile b/Makefile
index c04c4cf..5e8822a 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,5 @@
TOP := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
CWD := $(shell pwd)
-PYTHON2 ?= python2
PYTHON3 ?= python3
COVERAGE ?= 1
DEFAULT_COVERAGEOPTS = --with-coverage --cover-erase --cover-branches --cover-package=curtin --cover-inclusive
@@ -16,11 +15,11 @@ target_dirs ?= curtin tests tools
build:
bin/curtin: curtin/pack.py tools/write-curtin
- $(PYTHON) tools/write-curtin bin/curtin
+ $(PYTHON3) tools/write-curtin bin/curtin
check: unittest
-style-check: pep8 pyflakes pyflakes3
+style-check: pep8 pyflakes3
coverage: coverageopts ?= $(DEFAULT_COVERAGEOPTS)
coverage: unittest
@@ -28,26 +27,15 @@ coverage: unittest
pep8:
@$(CWD)/tools/run-pep8
-pyflakes:
- $(PYTHON2) -m pyflakes $(target_dirs)
-
-pyflakes3:
+pyflakes pyflakes3:
$(PYTHON3) -m pyflakes $(target_dirs)
-pylint:
- $(PYTHON2) -m pylint $(pylintopts) $(target_dirs)
-
-pylint3:
+pylint pylint3:
$(PYTHON3) -m pylint $(pylintopts) $(target_dirs)
-unittest2:
- $(PYTHON2) -m nose $(coverageopts) $(noseopts) tests/unittests
-
-unittest3:
+unittest unittest3:
$(PYTHON3) -m nose $(coverageopts) $(noseopts) tests/unittests
-unittest: unittest2 unittest3
-
schema-validate:
@$(CWD)/tools/schema-validate-storage
@@ -55,7 +43,7 @@ docs: check-doc-deps
make -C doc html
check-doc-deps:
- @which sphinx-build && $(PYTHON) -c 'import sphinx_rtd_theme' || \
+ @which sphinx-build && $(PYTHON3) -c 'import sphinx_rtd_theme' || \
{ echo "Missing doc dependencies. Install with:"; \
pkgs="python3-sphinx-rtd-theme python3-sphinx"; \
echo sudo apt-get install -qy $$pkgs ; exit 1; }
diff --git a/curtin/block/__init__.py b/curtin/block/__init__.py
index 1f9bb60..b6802fb 100644
--- a/curtin/block/__init__.py
+++ b/curtin/block/__init__.py
@@ -13,6 +13,7 @@ from curtin.block import lvm
from curtin.block import multipath
from curtin.log import LOG
from curtin.udev import udevadm_settle, udevadm_info
+from curtin.util import NotExclusiveError
from curtin import storage_config
@@ -733,6 +734,7 @@ def get_root_device(dev, paths=None):
LOG.debug('Searching for filesystem on %s containing one of: %s',
dev, paths)
partitions = get_pardevs_on_blockdevs(dev)
+ LOG.debug('Known partitions %s', list(partitions.keys()))
target = None
tmp_mount = tempfile.mkdtemp()
for i in partitions:
@@ -1040,9 +1042,13 @@ 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
+ file_size = util.file_size(devname)
+ 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):
@@ -1083,9 +1089,20 @@ def is_extended_partition(device):
# within the first 4 partitions and will have a valid dos signature,
# because the format of the extended partition matches that of a real mbr
(parent_dev, part_number) = get_blockdev_for_partition(device)
- return (get_part_table_type(parent_dev) in ['dos', 'msdos'] and
- part_number is not None and int(part_number) <= 4 and
- check_dos_signature(device))
+ if (get_part_table_type(parent_dev) in ['dos', 'msdos'] and
+ part_number is not None and int(part_number) <= 4):
+ try:
+ return check_dos_signature(device)
+ except OSError as ose:
+ # Some older series have the extended partition block device but
+ # return ENXIO when attempting to read it. Make a best guess from
+ # the parent_dev.
+ if ose.errno == errno.ENXIO:
+ return check_dos_signature(parent_dev)
+ else:
+ raise
+ else:
+ return False
def is_zfs_member(device):
@@ -1159,7 +1176,7 @@ def exclusive_open(path, exclusive=True):
# python2 leaves fd open if there os.fdopen fails
if fd_needs_closing and sys.version_info.major == 2:
os.close(fd)
- except OSError:
+ except OSError as exc:
LOG.error("Failed to exclusively open path: %s", path)
holders = get_holders(path)
LOG.error('Device holders with exclusive access: %s', holders)
@@ -1167,7 +1184,10 @@ def exclusive_open(path, exclusive=True):
LOG.error('Device mounts: %s', mount_points)
fusers = util.fuser_mount(path)
LOG.error('Possible users of %s:\n%s', path, fusers)
- raise
+ if exclusive and exc.errno == errno.EBUSY:
+ raise NotExclusiveError from exc
+ else:
+ raise
def wipe_file(path, reader=None, buflen=4 * 1024 * 1024, exclusive=True):
@@ -1233,8 +1253,9 @@ def quick_zero(path, partitions=True, exclusive=True):
quick_zero(pt, partitions=False)
LOG.debug("wiping 1M on %s at offsets %s", path, offsets)
- return zero_file_at_offsets(path, offsets, buflen=buflen, count=count,
- exclusive=exclusive)
+ util.not_exclusive_retry(
+ zero_file_at_offsets,
+ path, offsets, buflen=buflen, count=count, exclusive=exclusive)
def zero_file_at_offsets(path, offsets, buflen=1024, count=1024, strict=False,
diff --git a/curtin/commands/apt_config.py b/curtin/commands/apt_config.py
index e02327a..0b8066b 100644
--- a/curtin/commands/apt_config.py
+++ b/curtin/commands/apt_config.py
@@ -601,14 +601,8 @@ def apply_apt_proxy_config(cfg, proxy_fname, config_fname):
LOG.debug("write apt proxy info to %s", proxy_fname)
util.write_file(proxy_fname, '\n'.join(proxies) + '\n')
elif os.path.isfile(proxy_fname):
- # When $ curtin apt-config is called with no proxy set, it makes
- # sense to remove the proxy file (if present). Having said that,
- # this code is also called automatically at the curthooks stage with an
- # empty configuration. Since the installation of external packages and
- # execution of unattended-upgrades (which happen after executing the
- # curthooks) need to use the proxy if specified, we must not let the
- # curthooks remove the proxy file.
- pass
+ util.del_file(proxy_fname)
+ LOG.debug("no apt proxy configured, removed %s", proxy_fname)
if cfg.get('conf', None):
LOG.debug("write apt config info to %s", config_fname)
@@ -638,12 +632,9 @@ def apply_apt_preferences(cfg, pref_fname):
prefs = cfg.get("preferences")
if not prefs:
- # When $ curtin apt-config is called with no preferences set, it makes
- # sense to remove the preferences file (if present). Having said that,
- # this code is also called automatically at the curthooks stage with an
- # empty configuration. Since the installation of packages (which
- # happens after executing the curthooks) needs to honor the preferences
- # set, we must not let the curthooks remove the preferences file.
+ if os.path.isfile(pref_fname):
+ util.del_file(pref_fname)
+ LOG.debug("no apt preferences configured, removed %s", pref_fname)
return
prefs_as_strings = [preference_to_str(pref) for pref in prefs]
LOG.debug("write apt preferences info to %s.", pref_fname)
diff --git a/curtin/commands/block_meta.py b/curtin/commands/block_meta.py
index 7988f3a..b00defe 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)
@@ -159,13 +159,17 @@ def write_image_to_disk(source, dev):
'--', source['uri'], devnode])
util.subp(['partprobe', devnode])
- udevadm_trigger([devnode])
- try:
- lvm.activate_volgroups()
- except util.ProcessExecutionError:
- # partial vg may not come up due to missing members, that's OK
- pass
- udevadm_settle()
+ for i in range(3):
+ # For images that contain block devices of interest to device mapper, a
+ # single round can sometimes not be enough to discover the mapped
+ # devices. So let's have an ugly retry loop.
+ udevadm_trigger([devnode])
+ try:
+ lvm.activate_volgroups()
+ except util.ProcessExecutionError:
+ # partial vg may not come up due to missing members, that's OK
+ pass
+ udevadm_settle()
# Images from MAAS have well-known/required paths present
# on the rootfs partition. Use these values to select the
@@ -649,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:
@@ -666,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)
@@ -854,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 p_size == 0 or p_start == 0:
+ # 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 79dfd39..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,
@@ -26,6 +26,23 @@ from curtin.storage_config import (
from curtin.udev import udevadm_settle
+def to_utf8_hex_notation(string: str) -> str:
+ ''' Convert a string into a valid ASCII string where all characters outside
+ the alphanumerical range (according to bytes.isalnum()) are translated to
+ their corresponding \\x notation. E.g.:
+ to_utf8_hex_notation("hello") => "hello"
+ to_utf8_hex_notation("réservée") => "r\\xc3\\xa9serv\\xc3\\xa9e"
+ to_utf8_hex_notation("sp ace") => "sp\\x20ace"
+ '''
+ result = ''
+ for c in bytearray(string, 'utf-8'):
+ if bytes([c]).isalnum():
+ result += bytes([c]).decode()
+ else:
+ result += f'\\x{c:02x}'
+ return result
+
+
@attr.s(auto_attribs=True)
class PartTableEntry:
# The order listed here matches the order sfdisk represents these fields
@@ -49,7 +66,11 @@ class PartTableEntry:
if v is not None:
r += ' {}={}'.format(a, v)
if self.name is not None:
- r += ' name="{}"'.format(self.name)
+ # Partition names are basically free-text fields. Injecting some
+ # characters such as '"', '\' and '\n' will result in lots of
+ # trouble. Fortunately, sfdisk supports \x notation, so we can
+ # rely on it.
+ r += ' name="{}"'.format(to_utf8_hex_notation(self.name))
if self.attrs:
r += ' attrs="{}"'.format(' '.join(self.attrs))
if self.bootable:
@@ -150,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
@@ -248,12 +270,24 @@ class DOSPartTable(SFDiskPartTable):
label = 'dos'
_extended = None
+ @staticmethod
+ def is_logical(action) -> bool:
+ flag = action.get('flag', None)
+ if flag == 'logical':
+ return True
+ # In some scenarios, a swap partition can be in the extended
+ # partition. When it does, the flag is set to 'swap'.
+ # In some other scenarios, a bootable partition can also be in the
+ # extended partition. This is not a supported use-case but is
+ # yet another scenario where flag is not set to 'logical'.
+ return action.get('number', 0) > 4
+
def add(self, action):
flag = action.get('flag', None)
start = action.get('offset', None)
if start is not None:
start = self.bytes2sectors(start)
- if flag == 'logical':
+ if self.is_logical(action):
if self._extended is None:
raise Exception("logical partition without extended partition")
prev = None
diff --git a/curtin/commands/install_grub.py b/curtin/commands/install_grub.py
index 38bf71a..03b4670 100644
--- a/curtin/commands/install_grub.py
+++ b/curtin/commands/install_grub.py
@@ -26,12 +26,13 @@ CMD_ARGUMENTS = (
GRUB_MULTI_INSTALL = '/usr/lib/grub/grub-multi-install'
-def get_grub_package_name(target_arch, uefi, rhel_ver=None):
+def get_grub_package_name(target_arch, uefi, rhel_ver=None, osfamily=None):
"""Determine the correct grub distro package name.
:param: target_arch: string specifying the target system architecture
:param: uefi: boolean indicating if system is booted via UEFI or not
:param: rhel_ver: string specifying the major Redhat version in use.
+ :param: osfamily: string specifying the target os family
:returns: tuple of strings, grub package name and grub target name
"""
if target_arch is None:
@@ -43,43 +44,64 @@ def get_grub_package_name(target_arch, uefi, rhel_ver=None):
if 'ppc64' in target_arch:
return ('grub-ieee1275', 'powerpc-ieee1275')
if uefi:
- if target_arch == 'amd64':
- grub_name = 'grub-efi-%s' % target_arch
- grub_target = "x86_64-efi"
- elif target_arch == 'x86_64':
- # centos 7+, no centos6 support
- # grub2-efi-x64 installs a signed grub bootloader
- grub_name = "grub2-efi-x64"
- grub_target = "x86_64-efi"
- elif target_arch == 'aarch64':
- # centos 7+, no centos6 support
- # grub2-efi-aa64 installs a signed grub bootloader
- grub_name = "grub2-efi-aa64"
- grub_target = "arm64-efi"
- elif target_arch == 'arm64':
- grub_name = 'grub-efi-%s' % target_arch
- grub_target = "arm64-efi"
- elif target_arch == 'i386':
- grub_name = 'grub-efi-ia32'
- grub_target = 'i386-efi'
- elif target_arch == 'riscv64':
- grub_name = 'grub-efi-riscv64'
- grub_target = 'riscv64-efi'
+ if osfamily == distro.DISTROS.redhat:
+ if target_arch == 'x86_64':
+ # centos 7+, no centos6 support
+ # grub2-efi-x64 installs a signed grub bootloader
+ grub_name = "grub2-efi-x64"
+ grub_target = "x86_64-efi"
+ elif target_arch == 'aarch64':
+ # centos 7+, no centos6 support
+ # grub2-efi-aa64 installs a signed grub bootloader
+ grub_name = "grub2-efi-aa64"
+ grub_target = "arm64-efi"
+ else:
+ raise ValueError('Unsupported RHEL version: %s', rhel_ver)
+ elif osfamily == distro.DISTROS.suse:
+ if target_arch == 'x86_64':
+ grub_target = "x86_64-efi"
+ elif target_arch == 'aarch64':
+ grub_target = "arm64-efi"
+ else:
+ raise ValueError('Unsupported SUSE arch: %s', target_arch)
+ grub_name = 'grub2-%s' % grub_target
else:
- raise ValueError('Unsupported UEFI arch: %s' % target_arch)
+ if target_arch == 'amd64':
+ grub_name = 'grub-efi-%s' % target_arch
+ grub_target = "x86_64-efi"
+ elif target_arch == 'arm64':
+ grub_name = 'grub-efi-%s' % target_arch
+ grub_target = "arm64-efi"
+ elif target_arch == 'i386':
+ grub_name = 'grub-efi-ia32'
+ grub_target = 'i386-efi'
+ elif target_arch == 'riscv64':
+ grub_name = 'grub-efi-riscv64'
+ grub_target = 'riscv64-efi'
+ else:
+ raise ValueError('Unsupported UEFI arch %s for OS %s' %
+ (target_arch, osfamily))
else:
grub_target = 'i386-pc'
- if target_arch in ['i386', 'amd64']:
- grub_name = 'grub-pc'
- elif target_arch == 'x86_64':
- if rhel_ver == '6':
- grub_name = 'grub'
- elif rhel_ver in ['7', '8', '9']:
- grub_name = 'grub2-pc'
+ if osfamily == distro.DISTROS.redhat:
+ if target_arch == 'x86_64':
+ if rhel_ver == '6':
+ grub_name = 'grub'
+ elif rhel_ver in ['7', '8', '9']:
+ grub_name = 'grub2-pc'
+ else:
+ raise ValueError('Unsupported RHEL version: %s', rhel_ver)
+ elif target_arch == 'i386':
+ grub_name = 'grub-pc'
else:
- raise ValueError('Unsupported RHEL version: %s', rhel_ver)
+ raise ValueError('Unsupported RHEL arch: %s', target_arch)
+ elif osfamily == distro.DISTROS.suse:
+ grub_name = 'grub2-i386-pc'
+ elif target_arch in ['i386', 'amd64']:
+ grub_name = 'grub-pc'
else:
- raise ValueError('Unsupported arch: %s' % target_arch)
+ raise ValueError('Unsupported arch %s for OS %s' %
+ (target_arch, osfamily))
return (grub_name, grub_target)
@@ -397,7 +419,9 @@ def install_grub(devices, target, uefi=None, grubcfg=None):
if distroinfo.family == distro.DISTROS.redhat else None)
check_target_arch_machine(target, arch=target_arch, uefi=uefi)
- grub_name, grub_target = get_grub_package_name(target_arch, uefi, rhel_ver)
+ grub_name, grub_target = get_grub_package_name(target_arch, uefi,
+ rhel_ver=rhel_ver,
+ osfamily=distroinfo.family)
grub_conf = get_grub_config_file(target, distroinfo.family)
new_params = get_carryover_params(distroinfo)
prepare_grub_dir(target, grub_conf)
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/curtin/udev.py b/curtin/udev.py
index fa4f940..c43e7a5 100644
--- a/curtin/udev.py
+++ b/curtin/udev.py
@@ -131,11 +131,25 @@ def udevadm_info(path=None):
def udev_all_block_device_properties():
import pyudev
- props = []
+ devices_props = []
c = pyudev.Context()
for device in c.list_devices(subsystem='block'):
- props.append(dict(device.properties))
- return props
+ # When dereferencing device[prop], pyudev calls bytes.decode(), which
+ # can fail if the value is invalid utf-8. We don't want a single
+ # invalid value to completely prevent probing. So we iterate
+ # over each value manually and ignore those which are invalid. We know
+ # that PARTNAME is subject to failures when accents and other special
+ # characters are used in a GPT partition name.
+ # See LP: 2017862
+ props = {}
+ for prop in device.properties:
+ try:
+ props[prop] = device.properties[prop]
+ except UnicodeDecodeError:
+ LOG.warning('ignoring property %s because it is not valid'
+ ' utf-8', prop)
+ devices_props.append(props)
+ return devices_props
# vi: ts=4 expandtab syntax=python
diff --git a/curtin/util.py b/curtin/util.py
index aaa6008..75d7545 100644
--- a/curtin/util.py
+++ b/curtin/util.py
@@ -2,7 +2,7 @@
import argparse
import collections
-from contextlib import contextmanager
+from contextlib import contextmanager, suppress
import errno
import json
import os
@@ -62,6 +62,11 @@ _DNS_REDIRECT_IP = None
BASIC_MATCHER = re.compile(r'\$\{([A-Za-z0-9_.]+)\}|\$([A-Za-z0-9_.]+)')
+class NotExclusiveError(OSError):
+ ''' Exception to raise when an exclusive open (i.e, O_EXCL) fails with
+ EBUSY '''
+
+
def _subp(args, data=None, rcs=None, env=None, capture=False,
combine_capture=False, shell=False, logstring=False,
decode="replace", target=None, cwd=None, log_captured=False,
@@ -1323,4 +1328,11 @@ def uses_systemd():
return _USES_SYSTEMD
+
+def not_exclusive_retry(fun, *args, **kwargs):
+ with suppress(NotExclusiveError):
+ return fun(*args, **kwargs)
+ time.sleep(1)
+ return fun(*args, **kwargs)
+
# vi: ts=4 expandtab syntax=python
diff --git a/test-requirements.txt b/test-requirements.txt
index 1970d03..0d93c02 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,3 +1,6 @@
+pip
+wheel
+virtualenv
jsonschema
mock
nose
diff --git a/tests/integration/test_block_meta.py b/tests/integration/test_block_meta.py
index b25d2ce..a2368e8 100644
--- a/tests/integration/test_block_meta.py
+++ b/tests/integration/test_block_meta.py
@@ -9,10 +9,10 @@ from parameterized import parameterized
import re
import sys
from typing import Optional
+from unittest import skipIf
import yaml
-from curtin import block, log, udev, util
-
+from curtin import block, compat, distro, log, udev, util
from curtin.commands.block_meta import _get_volume_fstype
from curtin.commands.block_meta_v2 import ONE_MIB_BYTES
@@ -26,11 +26,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])
@@ -117,7 +116,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']:
@@ -168,8 +167,7 @@ class StorageConfigBuilder:
def add_image(self, *, path, size, create=False, **kw):
if create:
- with open(path, "wb") as f:
- f.write(b"\0" * int(util.human2bytes(size)))
+ util.subp(['truncate', '-s', str(size), path])
action = self._add(type='image', path=path, size=size, **kw)
self.cur_image = action['id']
return action
@@ -311,15 +309,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 +426,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 +727,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 +878,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
@@ -1084,9 +1099,13 @@ class TestBlockMeta(IntegrationTestCase):
actual_name = sfdisk_info['partitions'][0]['name']
self.assertEqual(name, actual_name)
- def test_gpt_name_persistent(self):
+ @parameterized.expand([
+ ('random', CiTestCase.random_string(),),
+ # "écrasé" means "overwritten"
+ ('unicode', "'name' must not be écrasé/덮어쓴!"),
+ ])
+ def test_gpt_name_persistent(self, title, name):
self.img = self.tmp_path('image.img')
- name = self.random_string()
config = StorageConfigBuilder(version=2)
config.add_image(path=self.img, size='20M', ptable='gpt')
p1 = config.add_part(number=1, offset=1 << 20, size=18 << 20,
@@ -1124,6 +1143,8 @@ class TestBlockMeta(IntegrationTestCase):
actual_attrs = set(attrs_str.split(' '))
self.assertEqual(set(attrs), actual_attrs)
+ @skipIf(distro.lsb_release()['release'] < '18.04',
+ 'old sfdisk no attr support')
def test_gpt_set_multi_attr(self):
self.img = self.tmp_path('image.img')
config = StorageConfigBuilder(version=2)
@@ -1171,12 +1192,14 @@ class TestBlockMeta(IntegrationTestCase):
config = StorageConfigBuilder(version=2)
config.add_image(path=self.img, create=True, size='20M', ptable='gpt',
preserve=True)
+ # Set first-lba, and also a stub partition to keep older sfdisk happy.
script = '''\
label: gpt
-first-lba: 34'''.encode()
+first-lba: 34
+1MiB 1MiB L'''.encode()
with loop_dev(self.img) as dev:
cmd = ['sfdisk', dev]
- util.subp(cmd, data=script)
+ util.subp(cmd, data=script, capture=True)
config.add_part(number=1, offset=1 << 20, size=1 << 20)
self.run_bm(config.render())
@@ -1193,9 +1216,11 @@ first-lba: 34'''.encode()
config = StorageConfigBuilder(version=2)
config.add_image(path=self.img, create=True, size='20M', ptable='gpt',
preserve=True)
+ # Set last-lba, and also a stub partition to keep older sfdisk happy.
script = '''\
label: gpt
-last-lba: 10240'''.encode()
+last-lba: 10240
+1MiB 1MiB L'''.encode()
with loop_dev(self.img) as dev:
cmd = ['sfdisk', dev]
util.subp(cmd, data=script)
@@ -1210,6 +1235,8 @@ last-lba: 10240'''.encode()
# default is disk size in sectors - 17 KiB
self.assertEqual(10240, sfdisk_info['lastlba'])
+ @skipIf(distro.lsb_release()['release'] < '18.04',
+ 'old sfdisk has no table-length support')
def test_gpt_table_length_persistent(self):
self.img = self.tmp_path('image.img')
config = StorageConfigBuilder(version=2)
diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py
index 819e2c5..ffb2775 100644
--- a/tests/unittests/helpers.py
+++ b/tests/unittests/helpers.py
@@ -85,6 +85,7 @@ class CiTestCase(TestCase):
handler = logging.StreamHandler(self.logs)
handler.setFormatter(formatter)
self.old_handlers = self.logger.handlers
+ self.logger.setLevel(logging.DEBUG)
self.logger.handlers = [handler]
if self.allowed_subp is True:
@@ -152,8 +153,8 @@ class CiTestCase(TestCase):
return os.path.normpath(
os.path.abspath(os.path.sep.join((_dir, path))))
- @classmethod
- def random_string(cls, length=8):
+ @staticmethod
+ def random_string(length=8):
""" return a random lowercase string with default length of 8"""
return ''.join(
random.choice(string.ascii_lowercase) for _ in range(length))
diff --git a/tests/unittests/test_block_lvm.py b/tests/unittests/test_block_lvm.py
index fc6130a..315281d 100644
--- a/tests/unittests/test_block_lvm.py
+++ b/tests/unittests/test_block_lvm.py
@@ -76,12 +76,12 @@ class TestBlockLvm(CiTestCase):
@mock.patch('curtin.block.lvm.distro')
def test_lvm_scan(self, mock_distro, mock_util, mock_lvmetad):
"""check that lvm_scan formats commands correctly for each release"""
- cmds = [['pvscan'], ['vgscan']]
for (count, (codename, lvmetad_status, use_cache)) in enumerate(
[('precise', False, False),
('trusty', False, False),
('xenial', False, False), ('xenial', True, True),
(None, True, True), (None, False, False)]):
+ cmds = [['pvscan'], ['vgscan']]
mock_distro.lsb_release.return_value = {'codename': codename}
mock_lvmetad.return_value = lvmetad_status
lvm.lvm_scan()
@@ -92,7 +92,7 @@ class TestBlockLvm(CiTestCase):
calls = [mock.call(cmd, capture=True) for cmd in expected]
self.assertEqual(len(expected), len(mock_util.subp.call_args_list))
- mock_util.subp.has_calls(calls)
+ mock_util.subp.assert_has_calls(calls)
mock_util.subp.reset_mock()
@mock.patch('curtin.block.lvm.lvmetad_running')
@@ -112,7 +112,7 @@ class TestBlockLvm(CiTestCase):
expected = [cmd + cmd_filter for cmd in cmds]
calls = [mock.call(cmd, capture=True) for cmd in expected]
self.assertEqual(len(expected), len(mock_util.subp.call_args_list))
- mock_util.subp.has_calls(calls)
+ mock_util.subp.assert_has_calls(calls)
class TestBlockLvmMultipathFilter(CiTestCase):
diff --git a/tests/unittests/test_commands_block_meta.py b/tests/unittests/test_commands_block_meta.py
index 82530ff..5599886 100644
--- a/tests/unittests/test_commands_block_meta.py
+++ b/tests/unittests/test_commands_block_meta.py
@@ -25,6 +25,34 @@ def random_uuid():
empty_context = block_meta.BlockMetaContext({})
+class TestToUTF8HexNotation(CiTestCase):
+ def test_alpha(self):
+ self.assertEqual(
+ block_meta_v2.to_utf8_hex_notation("HelloWorld"), "HelloWorld")
+
+ def test_alphanum(self):
+ self.assertEqual(
+ block_meta_v2.to_utf8_hex_notation("Hello1234"), "Hello1234")
+
+ def test_alnum_space(self):
+ self.assertEqual(
+ block_meta_v2.to_utf8_hex_notation("Hello 1234"),
+ "Hello\\x201234")
+
+ def test_with_accent(self):
+ # '\xe9'.isalpha(), which is equivalent to 'é'.isalpha(), is True
+ # because 'é' is considered a letter according to the unicode standard.
+ # b'\xe9'.isalpha(), on the other hand, is False.
+ self.assertEqual(
+ block_meta_v2.to_utf8_hex_notation("réservée"),
+ "r\\xc3\\xa9serv\\xc3\\xa9e")
+
+ def test_hangul(self):
+ self.assertEqual(
+ block_meta_v2.to_utf8_hex_notation("리눅스"),
+ '\\xeb\\xa6\\xac\\xeb\\x88\\x85\\xec\\x8a\\xa4')
+
+
class TestGetPathToStorageVolume(CiTestCase):
def setUp(self):
@@ -3050,14 +3078,20 @@ label: gpt
table.add(dict(number=1, offset=1 << 20, size=9 << 20, flag='boot',
partition_name=name))
type_id = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
+ to_hex = block_meta_v2.to_utf8_hex_notation
expected = f'''\
label: gpt
-1: start=2048 size=18432 type={type_id} name="{name}"'''
+1: start=2048 size=18432 type={type_id} name="{to_hex(name)}"'''
self.assertEqual(expected, table.render())
- def test_gpt_name_spaces(self):
- name = self.random_string() + " " + self.random_string()
+ def test_gpt_name_free_text(self):
+ name = 'my "분할" réservée'
+ expected_name = ''.join([
+ 'my',
+ '\\x20\\x22\\xeb\\xb6\\x84\\xed\\x95\\xa0\\x22',
+ '\\x20r\\xc3\\xa9serv\\xc3\\xa9e',
+ ])
table = block_meta_v2.GPTPartTable(512)
table.add(dict(number=1, offset=1 << 20, size=9 << 20, flag='boot',
partition_name=name))
@@ -3065,7 +3099,7 @@ label: gpt
expected = f'''\
label: gpt
-1: start=2048 size=18432 type={type_id} name="{name}"'''
+1: start=2048 size=18432 type={type_id} name="{expected_name}"'''
self.assertEqual(expected, table.render())
def test_gpt_attrs_none(self):
@@ -3255,10 +3289,25 @@ label: dos
number=1, start=2, size=3, type='04', bootable=False,
uuid=None, name=None, attrs=None)
pte.preserve({'uuid': uuid, 'name': name, 'attrs': attrs})
+ to_hex = block_meta_v2.to_utf8_hex_notation
expected = f'1: start=2 size=3 type=04 uuid={uuid} ' + \
- f'name="{name}" attrs="{attrs}"'
+ f'name="{to_hex(name)}" attrs="{attrs}"'
self.assertEqual(expected, pte.render())
+ def test_v2_dos_is_logical(self):
+ action = {"flag": "logical"}
+ self.assertTrue(block_meta_v2.DOSPartTable.is_logical(action))
+ action = {"flag": "logical", "number": 5}
+ self.assertTrue(block_meta_v2.DOSPartTable.is_logical(action))
+ action = {"flag": "swap", "number": 2}
+ self.assertFalse(block_meta_v2.DOSPartTable.is_logical(action))
+ action = {"flag": "swap", "number": 5}
+ self.assertTrue(block_meta_v2.DOSPartTable.is_logical(action))
+ action = {"flag": "boot", "number": 5}
+ self.assertTrue(block_meta_v2.DOSPartTable.is_logical(action))
+ action = {"flag": "swap"}
+ self.assertFalse(block_meta_v2.DOSPartTable.is_logical(action))
+
class TestPartitionNeedsResize(CiTestCase):
diff --git a/tests/unittests/test_commands_install_grub.py b/tests/unittests/test_commands_install_grub.py
index ab52505..00004d7 100644
--- a/tests/unittests/test_commands_install_grub.py
+++ b/tests/unittests/test_commands_install_grub.py
@@ -16,120 +16,191 @@ class TestGetGrubPackageName(CiTestCase):
target_arch = 'ppc64le'
uefi = False
rhel_ver = None
+ osfamily = distro.DISTROS.debian
self.assertEqual(
('grub-ieee1275', 'powerpc-ieee1275'),
- install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+ install_grub.get_grub_package_name(target_arch, uefi, rhel_ver,
+ osfamily))
def test_uefi_debian_amd64(self):
target_arch = 'amd64'
uefi = True
rhel_ver = None
+ osfamily = distro.DISTROS.debian
self.assertEqual(
('grub-efi-amd64', 'x86_64-efi'),
- install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+ install_grub.get_grub_package_name(target_arch, uefi, rhel_ver,
+ osfamily))
def test_uefi_rhel7_amd64(self):
target_arch = 'x86_64'
uefi = True
rhel_ver = '7'
+ osfamily = distro.DISTROS.redhat
self.assertEqual(
('grub2-efi-x64', 'x86_64-efi'),
- install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+ install_grub.get_grub_package_name(target_arch, uefi, rhel_ver,
+ osfamily))
def test_uefi_rhel8_amd64(self):
target_arch = 'x86_64'
uefi = True
rhel_ver = '8'
+ osfamily = distro.DISTROS.redhat
self.assertEqual(
('grub2-efi-x64', 'x86_64-efi'),
- install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+ install_grub.get_grub_package_name(target_arch, uefi, rhel_ver,
+ osfamily))
+
+ def test_uefi_suse_amd64(self):
+ target_arch = 'x86_64'
+ uefi = True
+ rhel_ver = None
+ osfamily = distro.DISTROS.suse
+ self.assertEqual(
+ ('grub2-x86_64-efi', 'x86_64-efi'),
+ install_grub.get_grub_package_name(target_arch, uefi, rhel_ver,
+ osfamily))
def test_uefi_rhel7_arm64(self):
target_arch = 'aarch64'
uefi = True
rhel_ver = '7'
+ osfamily = distro.DISTROS.redhat
self.assertEqual(
('grub2-efi-aa64', 'arm64-efi'),
- install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+ install_grub.get_grub_package_name(target_arch, uefi, rhel_ver,
+ osfamily))
def test_uefi_rhel8_arm64(self):
target_arch = 'aarch64'
uefi = True
rhel_ver = '8'
+ osfamily = distro.DISTROS.redhat
self.assertEqual(
('grub2-efi-aa64', 'arm64-efi'),
- install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+ install_grub.get_grub_package_name(target_arch, uefi, rhel_ver,
+ osfamily))
def test_uefi_debian_arm64(self):
target_arch = 'arm64'
uefi = True
rhel_ver = None
+ osfamily = distro.DISTROS.debian
self.assertEqual(
('grub-efi-arm64', 'arm64-efi'),
- install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+ install_grub.get_grub_package_name(target_arch, uefi, rhel_ver,
+ osfamily))
+
+ def test_uefi_suse_arm64(self):
+ target_arch = 'aarch64'
+ uefi = True
+ rhel_ver = None
+ osfamily = distro.DISTROS.suse
+ self.assertEqual(
+ ('grub2-arm64-efi', 'arm64-efi'),
+ install_grub.get_grub_package_name(target_arch, uefi, rhel_ver,
+ osfamily))
def test_uefi_debian_i386(self):
target_arch = 'i386'
uefi = True
rhel_ver = None
+ osfamily = distro.DISTROS.debian
self.assertEqual(
('grub-efi-ia32', 'i386-efi'),
- install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+ install_grub.get_grub_package_name(target_arch, uefi, rhel_ver,
+ osfamily))
def test_debian_amd64(self):
target_arch = 'amd64'
uefi = False
rhel_ver = None
+ osfamily = distro.DISTROS.debian
self.assertEqual(
('grub-pc', 'i386-pc'),
- install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+ install_grub.get_grub_package_name(target_arch, uefi, rhel_ver,
+ osfamily))
def test_rhel6_amd64(self):
target_arch = 'x86_64'
uefi = False
rhel_ver = '6'
+ osfamily = distro.DISTROS.redhat
self.assertEqual(
('grub', 'i386-pc'),
- install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+ install_grub.get_grub_package_name(target_arch, uefi, rhel_ver,
+ osfamily))
def test_rhel7_amd64(self):
target_arch = 'x86_64'
uefi = False
rhel_ver = '7'
+ osfamily = distro.DISTROS.redhat
self.assertEqual(
('grub2-pc', 'i386-pc'),
- install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+ install_grub.get_grub_package_name(target_arch, uefi, rhel_ver,
+ osfamily))
def test_rhel8_amd64(self):
target_arch = 'x86_64'
uefi = False
rhel_ver = '8'
+ osfamily = distro.DISTROS.redhat
self.assertEqual(
('grub2-pc', 'i386-pc'),
- install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+ install_grub.get_grub_package_name(target_arch, uefi, rhel_ver,
+ osfamily))
+
+ def test_suse_amd64(self):
+ target_arch = 'x86_64'
+ uefi = False
+ rhel_ver = None
+ osfamily = distro.DISTROS.suse
+ self.assertEqual(
+ ('grub2-i386-pc', 'i386-pc'),
+ install_grub.get_grub_package_name(target_arch, uefi, rhel_ver,
+ osfamily))
def test_debian_i386(self):
target_arch = 'i386'
uefi = False
rhel_ver = None
+ osfamily = distro.DISTROS.debian
self.assertEqual(
('grub-pc', 'i386-pc'),
- install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+ install_grub.get_grub_package_name(target_arch, uefi, rhel_ver,
+ osfamily))
+
+ def test_suse_i386(self):
+ target_arch = 'i386'
+ uefi = False
+ rhel_ver = None
+ osfamily = distro.DISTROS.suse
+ self.assertEqual(
+ ('grub2-i386-pc', 'i386-pc'),
+ install_grub.get_grub_package_name(target_arch, uefi, rhel_ver,
+ osfamily))
def test_invalid_rhel_version(self):
+ osfamily = distro.DISTROS.redhat
with self.assertRaises(ValueError):
install_grub.get_grub_package_name('x86_64', uefi=False,
- rhel_ver='5')
+ rhel_ver='5', osfamily=osfamily)
def test_invalid_arch(self):
+ osfamily = distro.DISTROS.debian
with self.assertRaises(ValueError):
install_grub.get_grub_package_name(self.random_string(),
- uefi=False, rhel_ver=None)
+ uefi=False, rhel_ver=None,
+ osfamily=osfamily)
def test_invalid_arch_uefi(self):
+ osfamily = distro.DISTROS.debian
with self.assertRaises(ValueError):
install_grub.get_grub_package_name(self.random_string(),
- uefi=True, rhel_ver=None)
+ uefi=True, rhel_ver=None,
+ osfamily=osfamily)
class TestGetGrubConfigFile(CiTestCase):
@@ -1095,7 +1166,8 @@ class TestInstallGrub(CiTestCase):
self.m_distro_get_distroinfo.assert_called_with(target=self.target)
self.m_distro_get_architecture.assert_called_with(target=self.target)
self.assertEqual(0, self.m_distro_rpm_get_dist_id.call_count)
- self.m_get_grub_package_name.assert_called_with('amd64', uefi, None)
+ self.m_get_grub_package_name.assert_called_with('amd64', uefi, None,
+ 'debian')
self.m_get_grub_config_file.assert_called_with(self.target,
self.distroinfo.family)
self.m_get_carryover_params.assert_called_with(self.distroinfo)
@@ -1136,7 +1208,8 @@ class TestInstallGrub(CiTestCase):
self.m_distro_get_distroinfo.assert_called_with(target=self.target)
self.m_distro_get_architecture.assert_called_with(target=self.target)
self.assertEqual(0, self.m_distro_rpm_get_dist_id.call_count)
- self.m_get_grub_package_name.assert_called_with('amd64', uefi, None)
+ self.m_get_grub_package_name.assert_called_with('amd64', uefi, None,
+ 'debian')
self.m_get_grub_config_file.assert_called_with(self.target,
self.distroinfo.family)
self.m_get_carryover_params.assert_called_with(self.distroinfo)
@@ -1178,7 +1251,8 @@ class TestInstallGrub(CiTestCase):
self.m_distro_get_distroinfo.assert_called_with(target=self.target)
self.m_distro_get_architecture.assert_called_with(target=self.target)
self.assertEqual(0, self.m_distro_rpm_get_dist_id.call_count)
- self.m_get_grub_package_name.assert_called_with('amd64', uefi, None)
+ self.m_get_grub_package_name.assert_called_with('amd64', uefi, None,
+ 'debian')
self.m_get_grub_config_file.assert_called_with(self.target,
self.distroinfo.family)
self.m_get_carryover_params.assert_called_with(self.distroinfo)
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)
diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py
index a111ea1..6a0c951 100644
--- a/tests/unittests/test_util.py
+++ b/tests/unittests/test_util.py
@@ -1158,4 +1158,43 @@ class TestSanitizeSource(CiTestCase):
self.assertEqual(expected, result)
+class TestNotExclusiveRetry(CiTestCase):
+ @mock.patch('curtin.util.time.sleep')
+ def test_not_exclusive_retry_success(self, sleep):
+ f = mock.Mock(return_value='success')
+
+ self.assertEqual(util.not_exclusive_retry(f, 1, 2, 3), 'success')
+ sleep.assert_not_called()
+
+ @mock.patch('curtin.util.time.sleep')
+ def test_not_exclusive_retry_failed(self, sleep):
+ f = mock.Mock(side_effect=OSError)
+
+ with self.assertRaises(OSError):
+ util.not_exclusive_retry(f, 1, 2, 3)
+ sleep.assert_not_called()
+
+ @mock.patch('curtin.util.time.sleep')
+ def test_not_exclusive_retry_not_exclusive_once_then_success(self, sleep):
+ f = mock.Mock(side_effect=[util.NotExclusiveError, 'success'])
+
+ self.assertEqual(util.not_exclusive_retry(f, 1, 2, 3), 'success')
+ sleep.assert_called_once()
+
+ @mock.patch('curtin.util.time.sleep')
+ def test_not_exclusive_retry_not_exclusive_twice(self, sleep):
+ f = mock.Mock(side_effect=[util.NotExclusiveError] * 2)
+
+ with self.assertRaises(util.NotExclusiveError):
+ util.not_exclusive_retry(f, 1, 2, 3)
+ sleep.assert_called_once()
+
+ @mock.patch('curtin.util.time.sleep')
+ def test_not_exclusive_retry_not_exclusive_once_then_error(self, sleep):
+ f = mock.Mock(side_effect=[util.NotExclusiveError, OSError])
+
+ with self.assertRaises(OSError):
+ util.not_exclusive_retry(f, 1, 2, 3)
+ sleep.assert_called_once()
+
# vi: ts=4 expandtab syntax=python
diff --git a/tools/vmtest-create-static-images b/tools/vmtest-create-static-images
index d236608..3c20ef3 100755
--- a/tools/vmtest-create-static-images
+++ b/tools/vmtest-create-static-images
@@ -34,7 +34,8 @@ trap cleanup EXIT
mkpart primary 0% 25% \
mkpart primary 25% 100% \
set 2 lvm on
- sudo udevadm trigger --settle "${loopdev}"
+ sudo udevadm trigger "${loopdev}"
+ sudo udevadm settle
# create LVM volumes
sudo pvcreate "${loopdev}"p2
diff --git a/tools/vmtest-system-setup b/tools/vmtest-system-setup
index 4596df5..c34e9aa 100755
--- a/tools/vmtest-system-setup
+++ b/tools/vmtest-system-setup
@@ -11,27 +11,37 @@ case "$(uname -m)" in
s390x) qemu="qemu-system-s390x";;
esac
-get_python_apt() {
- [[ "$1" < "21.04" ]] && echo python-apt
- [[ "$1" > "16.04" ]] && echo python3-apt
-}
-
DEPS=(
+ build-essential
cloud-image-utils
+ git
make
net-tools
+ pep8
python3
+ python3-apt
python3-attr
+ python3-coverage
python3-jsonschema
python3-nose
+ python3-oauthlib
+ python3-pip
+ python3-pyflakes
+ python3-pytest
+ python3-pyudev
python3-simplestreams
+ python3-wheel
python3-yaml
- $(get_python_apt "$(lsb_release -sr)")
+ lvm2
+ ntfs-3g
ovmf
+ parted
simplestreams
$qemu
ubuntu-cloudimage-keyring
tgt
+ tox
+ wget
)
apt_get() {
Follow ups