image: Parse boolean string properties leniently

Glance returns image extra properties as strings. hw_boot_menu and
os_require_quiesce are declared with type=bool, so bool("false")
evaluates to True and an image with either property set to "false"
reports it as enabled.

hw_vif_multiqueue_enabled was switched to format.BoolStr to fix the
same problem (bug #1996033), but BoolStr accepts only "true" and
"false" and raises ValueError on anything else. Nova parses these
properties with oslo.versionedobjects' FlexibleBooleanField, so values
such as "yes" or "1" are valid and appear on real images; a single
such image makes listing images fail. hw_qemu_guest_agent was made a
plain string for the same reason, since it is commonly set to "yes"
(change I5b9714e4388df440bf881033283da4e8aa1db497).

Add a FlexibleBoolStr formatter that behaves like
oslo_utils.strutils.bool_from_string with its default arguments, as
FlexibleBooleanField does, so that the SDK interprets these properties
exactly as Nova will: recognised true strings are True and anything
else, including unrecognised values, is False. Use it for all three
properties. Unset properties remain None. The parser is implemented
locally to avoid adding oslo.utils as a dependency.
BoolStr is unchanged, as its callers parse API fields that are only
ever "true" or "false".

Closes-Bug: #2169133
Related-Bug: #1996033
Assisted-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Change-Id: Ib8b4c0fdbefe6ebd6cc856324eac78bd2ca72935
Signed-off-by: Tore Anderson <tore@redpill-linpro.com>
This commit is contained in:
Tore Anderson committed 2026-10-02 07:53:11 +02:00
1 parent ef55d7d166
commit 8368b5372e
7 files changed
+128 -16

No files matched your search

+19
View File
@@ -33,3 +33,22 @@ class BoolStr(Formatter[bool]):
return False
else:
raise ValueError(f"Unable to deserialize boolean string: {value}")
class FlexibleBoolStr(Formatter[bool]):
"""Leniently convert a boolean string to a boolean
Behaves like ``oslo_utils.strutils.bool_from_string`` with its default
arguments, which is how services such as Nova interpret these values:
recognised strings map to ``True`` or ``False`` and anything else is
``False``. This also means a single malformed value cannot prevent the
containing resource from being loaded.
"""
TRUE_STRINGS = frozenset(('1', 't', 'true', 'on', 'y', 'yes'))
@classmethod
def deserialize(cls, value: Any) -> bool:
if isinstance(value, bool):
return value
return str(value).strip().lower() in cls.TRUE_STRINGS
+9 -5
View File
@@ -231,14 +231,15 @@ class Image(resource.Resource, tag.TagMixin, _download.DownloadMixin):
#: In this case, the driver sets the number of queues equal to the
#: number of guest vCPUs. This makes the network performance scale
#: across a number of vCPUs.
#: Glance stores this extra property as a string; BoolStr parses
#: ``"false"`` correctly (``type=bool`` would treat it as True).
is_hw_vif_multiqueue_enabled = resource.Body(
'hw_vif_multiqueue_enabled',
type=format.BoolStr,
type=format.FlexibleBoolStr,
)
#: If true, enables the BIOS bootmenu.
is_hw_boot_menu_enabled = resource.Body('hw_boot_menu', type=bool)
is_hw_boot_menu_enabled = resource.Body(
'hw_boot_menu',
type=format.FlexibleBoolStr,
)
#: The virtual SCSI or IDE controller used by the hypervisor.
vmware_adaptertype = resource.Body('vmware_adaptertype')
#: A VMware GuestID which describes the operating system installed
@@ -255,7 +256,10 @@ class Image(resource.Resource, tag.TagMixin, _download.DownloadMixin):
#: to the instance.
hw_qemu_guest_agent = resource.Body('hw_qemu_guest_agent', type=str)
#: If true, require quiesce on snapshot via QEMU guest agent.
os_require_quiesce = resource.Body('os_require_quiesce', type=bool)
os_require_quiesce = resource.Body(
'os_require_quiesce',
type=format.FlexibleBoolStr,
)
#: The URL for the schema describing a virtual machine image.
schema = resource.Body('schema')
+3 -2
View File
@@ -111,8 +111,9 @@ def generate_fake_resource(
elif issubclass(target_type, float):
# float
base_attrs[name] = random.random() # noqa: S311
elif issubclass(target_type, bool) or issubclass(
target_type, _format.BoolStr
elif issubclass(
target_type,
bool | _format.BoolStr | _format.FlexibleBoolStr,
):
# bool
base_attrs[name] = random.choice([True, False]) # noqa: S311
+34 -9
View File
@@ -82,15 +82,15 @@ EXAMPLE: dict[str, Any] = {
'hw_watchdog_action': '44',
'os_command_line': '45',
'hw_vif_model': '46',
'hw_vif_multiqueue_enabled': True,
'hw_boot_menu': True,
'hw_vif_multiqueue_enabled': 'yes',
'hw_boot_menu': 'false',
'vmware_adaptertype': '47',
'vmware_ostype': '48',
'auto_disk_config': True,
'os_type': '49',
'os_admin_user': 'ubuntu',
'hw_qemu_guest_agent': 'yes',
'os_require_quiesce': True,
'os_require_quiesce': 'True',
}
@@ -224,11 +224,8 @@ class TestImage(base.TestCase):
self.assertEqual(EXAMPLE['hw_watchdog_action'], sot.hw_watchdog_action)
self.assertEqual(EXAMPLE['os_command_line'], sot.os_command_line)
self.assertEqual(EXAMPLE['hw_vif_model'], sot.hw_vif_model)
self.assertEqual(
EXAMPLE['hw_vif_multiqueue_enabled'],
sot.is_hw_vif_multiqueue_enabled,
)
self.assertEqual(EXAMPLE['hw_boot_menu'], sot.is_hw_boot_menu_enabled)
self.assertIs(True, sot.is_hw_vif_multiqueue_enabled)
self.assertIs(False, sot.is_hw_boot_menu_enabled)
self.assertEqual(EXAMPLE['vmware_adaptertype'], sot.vmware_adaptertype)
self.assertEqual(EXAMPLE['vmware_ostype'], sot.vmware_ostype)
self.assertEqual(EXAMPLE['auto_disk_config'], sot.has_auto_disk_config)
@@ -237,7 +234,35 @@ class TestImage(base.TestCase):
self.assertEqual(
EXAMPLE['hw_qemu_guest_agent'], sot.hw_qemu_guest_agent
)
self.assertEqual(EXAMPLE['os_require_quiesce'], sot.os_require_quiesce)
self.assertIs(True, sot.os_require_quiesce)
def test_make_it_boolean_strings(self):
sot = image.Image.existing(
id=IDENTIFIER,
hw_vif_multiqueue_enabled='false',
hw_boot_menu='on',
os_require_quiesce='no',
)
self.assertIs(False, sot.is_hw_vif_multiqueue_enabled)
self.assertIs(True, sot.is_hw_boot_menu_enabled)
self.assertIs(False, sot.os_require_quiesce)
def test_make_it_unrecognised_boolean_strings(self):
sot = image.Image.existing(
id=IDENTIFIER,
hw_vif_multiqueue_enabled='INVALID',
hw_boot_menu='',
os_require_quiesce='maybe',
)
self.assertIs(False, sot.is_hw_vif_multiqueue_enabled)
self.assertIs(False, sot.is_hw_boot_menu_enabled)
self.assertIs(False, sot.os_require_quiesce)
def test_make_it_unset_boolean_strings(self):
sot = image.Image.existing(id=IDENTIFIER)
self.assertIsNone(sot.is_hw_vif_multiqueue_enabled)
self.assertIsNone(sot.is_hw_boot_menu_enabled)
self.assertIsNone(sot.os_require_quiesce)
def test_deactivate(self):
sot = image.Image(**EXAMPLE)
+2
View File
@@ -39,6 +39,7 @@ class TestGetFake(base.TestCase):
d = resource.Body("d", type=_format.BoolStr)
e = resource.Body("e", type=dict)
f = resource.URI("path")
g = resource.Body("g", type=_format.FlexibleBoolStr)
class Bar(resource.Resource):
a = resource.Body("a", type=list, list_type=str)
@@ -52,6 +53,7 @@ class TestGetFake(base.TestCase):
self.assertIsInstance(foo.d, bool)
self.assertIsInstance(foo.e, dict)
self.assertIsInstance(foo.f, str)
self.assertIsInstance(foo.g, bool)
bar = fakes.generate_fake_resource(Bar)
self.assertIsInstance(bar.a, list)
+50
View File
@@ -27,3 +27,53 @@ class TestBoolStrFormatter(base.TestCase):
self.assertRaises(ValueError, format.BoolStr.deserialize, None)
self.assertRaises(ValueError, format.BoolStr.deserialize, '')
self.assertRaises(ValueError, format.BoolStr.deserialize, 'INVALID')
class TestFlexibleBoolStrFormatter(base.TestCase):
def test_deserialize_true(self):
for value in (
True,
'1',
't',
'T',
'true',
'True',
'TRUE',
'on',
'ON',
'y',
'Y',
'yes',
'Yes',
' yes ',
):
self.assertIs(
True, format.FlexibleBoolStr.deserialize(value), repr(value)
)
def test_deserialize_false(self):
for value in (
False,
'0',
'f',
'F',
'false',
'False',
'FALSE',
'off',
'OFF',
'n',
'N',
'no',
'No',
' no ',
):
self.assertIs(
False, format.FlexibleBoolStr.deserialize(value), repr(value)
)
def test_deserialize_unrecognised(self):
for value in (None, '', 'INVALID', '2', 'enabled', 'maybe'):
self.assertIs(
False, format.FlexibleBoolStr.deserialize(value), repr(value)
)
@@ -0,0 +1,11 @@
---
fixes:
- |
The ``hw_boot_menu`` and ``os_require_quiesce`` image properties are now
parsed as boolean strings, so a value of ``"false"`` is treated as
``False``. These properties and ``hw_vif_multiqueue_enabled`` now
interpret their values the same way as Nova: ``1``, ``t``, ``true``,
``on``, ``y`` and ``yes`` (case-insensitively) are ``True``, and any
other value is ``False``. An unrecognised value no longer raises an
exception that prevents the image from loading.
(`bug 2169133 <https://bugs.launchpad.net/bugs/2169133>`__)