4e5dbe2
From 014411b0984fb85588123912a12f3f1d39215c27 Mon Sep 17 00:00:00 2001
4e5dbe2
From: Adam Williamson <awilliam@redhat.com>
4e5dbe2
Date: Tue, 23 Jan 2024 11:24:00 -0800
4e5dbe2
Subject: [PATCH 21/21] Allow image size specification using any SI or IEC unit
4e5dbe2
4e5dbe2
Currently, oz only allows image sizes to be specified as integer
4e5dbe2
amounts of gibibytes or tebibytes (that's IEC power-of-two units).
4e5dbe2
This is unfortunately inflexible. Consider that storage media are
4e5dbe2
typically specified in SI power-of-ten sizes, so a USB stick may
4e5dbe2
be 16 gigabytes (SI power-of-ten GB) in size - that's
4e5dbe2
16,000,000,000 bytes. Let's say we want to build an image that
4e5dbe2
will fit on such a USB stick. oz will only allow us to build an
4e5dbe2
image of 15,032,385,536 bytes (14 gibibytes) or 16,106,127,360
4e5dbe2
bytes (15 gibibytes). So we're either slightly too big, or leaving
4e5dbe2
nearly a gigabyte on the table.
4e5dbe2
4e5dbe2
This allows the image size to be specified in the TDL with most
4e5dbe2
any IEC or SI unit suffix, from B (bytes) all the way up to YiB
4e5dbe2
(yobibytes). A size with no suffix or the suffix "G" is taken as
4e5dbe2
gibibytes and a size with the suffix "T" is taken as tebibytes,
4e5dbe2
as before, but other ambiguous suffixes are not accepted. All
4e5dbe2
casing is accepted. Behind the scenes, we convert the size to
4e5dbe2
bytes and specify it that way in the libvirt XML when creating
4e5dbe2
the image in _internal_generate_diskimage.
4e5dbe2
4e5dbe2
This does change the interface of generate_diskimage(), by making
4e5dbe2
the unit for the size argument bytes instead of gibibytes. I can't
4e5dbe2
see a clean way to avoid this while allowing flexibility. I have
4e5dbe2
checked, and AFAICT, among active projects, only oz itself and
4e5dbe2
the ImageFactory TinMan plugin call this function. The TinMan
4e5dbe2
plugin will need a trivial change to its fallback default value.
4e5dbe2
4e5dbe2
Signed-off-by: Adam Williamson <awilliam@redhat.com>
4e5dbe2
---
4e5dbe2
 oz/Fedora.py                                  |  6 +--
4e5dbe2
 oz/Guest.py                                   | 16 +++----
4e5dbe2
 oz/TDL.py                                     | 47 +++++++++++++------
4e5dbe2
 oz/Windows.py                                 |  8 ++--
4e5dbe2
 oz/ozutil.py                                  | 17 +++++++
4e5dbe2
 oz/tdl.rng                                    |  2 +-
4e5dbe2
 tests/guest/test_guest.py                     |  3 +-
4e5dbe2
 ... => test-58-disk-size-tebibyte-compat.tdl} |  0
4e5dbe2
 tests/tdl/test-63-disk-size-exbibyte.tdl      | 15 ++++++
4e5dbe2
 tests/tdl/test-64-disk-size-zettabyte.tdl     | 15 ++++++
4e5dbe2
 tests/tdl/test-65-disk-size-byte.tdl          | 15 ++++++
4e5dbe2
 tests/tdl/test_tdl.py                         |  5 +-
4e5dbe2
 12 files changed, 117 insertions(+), 32 deletions(-)
4e5dbe2
 rename tests/tdl/{test-58-disk-size-terabyte.tdl => test-58-disk-size-tebibyte-compat.tdl} (100%)
4e5dbe2
 create mode 100644 tests/tdl/test-63-disk-size-exbibyte.tdl
4e5dbe2
 create mode 100644 tests/tdl/test-64-disk-size-zettabyte.tdl
4e5dbe2
 create mode 100644 tests/tdl/test-65-disk-size-byte.tdl
4e5dbe2
4e5dbe2
diff --git a/oz/Fedora.py b/oz/Fedora.py
4e5dbe2
index 89ecca8..c6152f1 100644
4e5dbe2
--- a/oz/Fedora.py
4e5dbe2
+++ b/oz/Fedora.py
4e5dbe2
@@ -298,11 +298,11 @@ class FedoraGuest(oz.RedHat.RedHatLinuxCDYumGuest):
4e5dbe2
         initrdline += " inst.nosave=output_ks"
4e5dbe2
         self._modify_isolinux(initrdline)
4e5dbe2
 
4e5dbe2
-    def generate_diskimage(self, size=10, force=False):
4e5dbe2
+    def generate_diskimage(self, size=10*1024*1024*1024, force=False):
4e5dbe2
         """
4e5dbe2
         Method to generate a diskimage.  By default, a blank diskimage of
4e5dbe2
-        10GB will be created; the caller can override this with the size
4e5dbe2
-        parameter, specified in GB.  If force is False (the default), then
4e5dbe2
+        10 GiB will be created; the caller can override this with the size
4e5dbe2
+        parameter, specified in bytes.  If force is False (the default), then
4e5dbe2
         a diskimage will not be created if a cached JEOS is found.  If
4e5dbe2
         force is True, a diskimage will be created regardless of whether a
4e5dbe2
         cached JEOS exists.  See the oz-install man page for more
4e5dbe2
diff --git a/oz/Guest.py b/oz/Guest.py
4e5dbe2
index 249cce0..8a4e485 100644
4e5dbe2
--- a/oz/Guest.py
4e5dbe2
+++ b/oz/Guest.py
4e5dbe2
@@ -571,7 +571,7 @@ class Guest(object):
4e5dbe2
 
4e5dbe2
         return xml
4e5dbe2
 
4e5dbe2
-    def _internal_generate_diskimage(self, size=10, force=False,
4e5dbe2
+    def _internal_generate_diskimage(self, size=10*1024*1024*1024, force=False,
4e5dbe2
                                      create_partition=False,
4e5dbe2
                                      image_filename=None,
4e5dbe2
                                      backing_filename=None):
4e5dbe2
@@ -587,7 +587,7 @@ class Guest(object):
4e5dbe2
             # we'll copy the JEOS itself later on
4e5dbe2
             return
4e5dbe2
 
4e5dbe2
-        self.log.info("Generating %dGB diskimage for %s", size, self.tdl.name)
4e5dbe2
+        self.log.info("Generating %s diskimage for %s", oz.ozutil.sizeof_fmt(int(size)), self.tdl.name)
4e5dbe2
 
4e5dbe2
         diskimage = self.diskimage
4e5dbe2
         if image_filename:
4e5dbe2
@@ -628,17 +628,17 @@ class Guest(object):
4e5dbe2
             # allows creation without an explicit capacity element.
4e5dbe2
             qcow_size = oz.ozutil.check_qcow_size(backing_filename)
4e5dbe2
             if qcow_size:
4e5dbe2
-                capacity = qcow_size / 1024 / 1024 / 1024
4e5dbe2
+                capacity = qcow_size
4e5dbe2
                 backing_format = 'qcow2'
4e5dbe2
             else:
4e5dbe2
-                capacity = os.path.getsize(backing_filename) / 1024 / 1024 / 1024
4e5dbe2
+                capacity = os.path.getsize(backing_filename)
4e5dbe2
                 backing_format = 'raw'
4e5dbe2
             backing = oz.ozutil.lxml_subelement(vol, "backingStore")
4e5dbe2
             oz.ozutil.lxml_subelement(backing, "path", backing_filename)
4e5dbe2
             oz.ozutil.lxml_subelement(backing, "format", None,
4e5dbe2
                                       {"type": backing_format})
4e5dbe2
 
4e5dbe2
-        oz.ozutil.lxml_subelement(vol, "capacity", str(int(capacity)), {'unit': 'G'})
4e5dbe2
+        oz.ozutil.lxml_subelement(vol, "capacity", str(int(capacity)), {'unit': 'B'})
4e5dbe2
         vol_xml = lxml.etree.tostring(vol, pretty_print=True, encoding="unicode")
4e5dbe2
 
4e5dbe2
         # sigh.  Yes, this is racy; if a pool is defined during this loop, we
4e5dbe2
@@ -718,11 +718,11 @@ class Guest(object):
4e5dbe2
                 g_handle.create_msdos_partition_table()
4e5dbe2
                 g_handle.cleanup()
4e5dbe2
 
4e5dbe2
-    def generate_diskimage(self, size=10, force=False):
4e5dbe2
+    def generate_diskimage(self, size=10*1024*1024*1024, force=False):
4e5dbe2
         """
4e5dbe2
         Method to generate a diskimage.  By default, a blank diskimage of
4e5dbe2
-        10GB will be created; the caller can override this with the size
4e5dbe2
-        parameter, specified in GB.  If force is False (the default), then
4e5dbe2
+        10 GiB will be created; the caller can override this with the size
4e5dbe2
+        parameter, specified in bytes.  If force is False (the default), then
4e5dbe2
         a diskimage will not be created if a cached JEOS is found.  If
4e5dbe2
         force is True, a diskimage will be created regardless of whether a
4e5dbe2
         cached JEOS exists.  See the oz-install man page for more
4e5dbe2
diff --git a/oz/TDL.py b/oz/TDL.py
4e5dbe2
index cb373cb..aee74a3 100644
4e5dbe2
--- a/oz/TDL.py
4e5dbe2
+++ b/oz/TDL.py
4e5dbe2
@@ -327,20 +327,39 @@ class TDL(object):
4e5dbe2
             # a sensible default
4e5dbe2
             return None
4e5dbe2
 
4e5dbe2
-        match = re.match(r'([0-9]*) *([GT]?)$', size)
4e5dbe2
-        if not match or len(match.groups()) != 2:
4e5dbe2
-            raise oz.OzException.OzException("Invalid disk size; it must be specified as a size in gigabytes, optionally suffixed with 'G' or 'T'")
4e5dbe2
-
4e5dbe2
-        number = match.group(1)
4e5dbe2
-        suffix = match.group(2)
4e5dbe2
-
4e5dbe2
-        if not suffix or suffix == 'G':
4e5dbe2
-            # for backwards compatibility, we assume G when there is no suffix
4e5dbe2
-            size = number
4e5dbe2
-        elif suffix == 'T':
4e5dbe2
-            size = str(int(number) * 1024)
4e5dbe2
-
4e5dbe2
-        return size
4e5dbe2
+        # drop spaces and downcase
4e5dbe2
+        size = size.replace(" ", "").lower()
4e5dbe2
+        # isolate digits
4e5dbe2
+        number = ""
4e5dbe2
+        suffix = ""
4e5dbe2
+        for (idx, char) in enumerate(size):
4e5dbe2
+            if char.isdigit():
4e5dbe2
+                number += char
4e5dbe2
+            else:
4e5dbe2
+                suffix = size[idx:]
4e5dbe2
+                break
4e5dbe2
+
4e5dbe2
+        if not suffix:
4e5dbe2
+            # for backwards compatibility, we assume GiB when there is no suffix
4e5dbe2
+            suffix = "gib"
4e5dbe2
+
4e5dbe2
+        # also for backwards compatibility with an earlier attempt to support
4e5dbe2
+        # suffixes, treat "T" and "G" as "TiB" and "GiB"
4e5dbe2
+        units = {"b": 1, "g": 2**30, "t": 2**40}
4e5dbe2
+        tenscale = 3
4e5dbe2
+        twoscale = 10
4e5dbe2
+        for (i, pref) in enumerate(("k", "m", "g", "t", "p", "e", "z", "y"), start=1):
4e5dbe2
+            # this is giving us {"gib": 2 ** 30, "gb": 10 ** 9}, etc
4e5dbe2
+            units[pref + "b"] = (10 ** (i*tenscale))
4e5dbe2
+            units[pref + "ib"] = (2 ** (i*twoscale))
4e5dbe2
+
4e5dbe2
+        factor = units.get(suffix)
4e5dbe2
+        if not number or not factor:
4e5dbe2
+            raise oz.OzException.OzException(
4e5dbe2
+                "Invalid disk size; it must be specified as an integer size with an optional SI or IEC unit suffix, e.g. '10TB' or '16GiB'"
4e5dbe2
+            )
4e5dbe2
+
4e5dbe2
+        return str(int(number) * factor)
4e5dbe2
 
4e5dbe2
     def _parse_commands(self, xpath):
4e5dbe2
         """
4e5dbe2
diff --git a/oz/Windows.py b/oz/Windows.py
4e5dbe2
index 78948ac..777a306 100644
4e5dbe2
--- a/oz/Windows.py
4e5dbe2
+++ b/oz/Windows.py
4e5dbe2
@@ -78,12 +78,12 @@ class Windows_v5(Windows):
4e5dbe2
                                            self.iso_contents],
4e5dbe2
                                           printfn=self.log.debug)
4e5dbe2
 
4e5dbe2
-    def generate_diskimage(self, size=10, force=False):
4e5dbe2
+    def generate_diskimage(self, size=10*1024*1024*1024, force=False):
4e5dbe2
         """
4e5dbe2
         Method to generate a diskimage.  By default, a blank diskimage of
4e5dbe2
-        10GB will be created; the caller can override this with the size
4e5dbe2
-        parameter, specified in GB.  If force is False (the default), then
4e5dbe2
-        a diskimage will not be created if a cached JEOS is found.  If
4e5dbe2
+        10 GiB will be created; the caller can override this with the size
4e5dbe2
+        parameter, specified in bytes.  If force is False (the default),
4e5dbe2
+        then a diskimage will not be created if a cached JEOS is found.  If
4e5dbe2
         force is True, a diskimage will be created regardless of whether a
4e5dbe2
         cached JEOS exists.  See the oz-install man page for more
4e5dbe2
         information about JEOS caching.
4e5dbe2
diff --git a/oz/ozutil.py b/oz/ozutil.py
4e5dbe2
index 689559f..be27d3f 100644
4e5dbe2
--- a/oz/ozutil.py
4e5dbe2
+++ b/oz/ozutil.py
4e5dbe2
@@ -1178,3 +1178,20 @@ def get_free_port():
4e5dbe2
     sock.close()
4e5dbe2
 
4e5dbe2
     return listen_port
4e5dbe2
+
4e5dbe2
+
4e5dbe2
+def sizeof_fmt(num, suffix="B"):
4e5dbe2
+    """
4e5dbe2
+    Give a convenient human-readable representation of a large size in
4e5dbe2
+    bytes. Initially by Fred Cirera:
4e5dbe2
+    https://web.archive.org/web/20111010015624/http://blogmag.net/blog/read/38/Print_human_readable_file_size
4e5dbe2
+    edited by multiple contributors at:
4e5dbe2
+    https://stackoverflow.com/questions/1094841
4e5dbe2
+    Per Richard Fontana this is too trivial to be copyrightable, so
4e5dbe2
+    there are no licensing concerns
4e5dbe2
+    """
4e5dbe2
+    for unit in ("", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"):
4e5dbe2
+        if abs(num) < 1024.0:
4e5dbe2
+            return "%3.1f%s%s" % (num, unit, suffix)
4e5dbe2
+        num /= 1024.0
4e5dbe2
+    return "%.1f%s%s" % (num, 'Yi', suffix)
4e5dbe2
diff --git a/oz/tdl.rng b/oz/tdl.rng
4e5dbe2
index dc29c9a..9ca94cc 100644
4e5dbe2
--- a/oz/tdl.rng
4e5dbe2
+++ b/oz/tdl.rng
4e5dbe2
@@ -325,7 +325,7 @@
4e5dbe2
 
4e5dbe2
   <define name='disk_size'>
4e5dbe2
     <data type="string">
4e5dbe2
-      <param name="pattern">([0-9]*) *([GT]?)</param>
4e5dbe2
+      <param name="pattern">([0-9]*) *([kKmMgGtTpPeEzZyY]?[iI]?[bB]?)</param>
4e5dbe2
     </data>
4e5dbe2
   </define>
4e5dbe2
 
4e5dbe2
diff --git a/tests/guest/test_guest.py b/tests/guest/test_guest.py
4e5dbe2
index 625eb67..85f6b03 100644
4e5dbe2
--- a/tests/guest/test_guest.py
4e5dbe2
+++ b/tests/guest/test_guest.py
4e5dbe2
@@ -352,7 +352,8 @@ def test_geteltorito_bogus_bootp(tmpdir):
4e5dbe2
 def test_init_guest():
4e5dbe2
     guest = setup_guest(tdlxml2)
4e5dbe2
 
4e5dbe2
-    assert guest.disksize == 20
4e5dbe2
+    # size without units is taken to be GiB
4e5dbe2
+    assert guest.disksize == 20*(2**30)
4e5dbe2
     assert guest.image_name() == 'tester'
4e5dbe2
     assert guest.output_image_path() in (
4e5dbe2
         # user's image storage
4e5dbe2
diff --git a/tests/tdl/test-58-disk-size-terabyte.tdl b/tests/tdl/test-58-disk-size-tebibyte-compat.tdl
4e5dbe2
similarity index 100%
4e5dbe2
rename from tests/tdl/test-58-disk-size-terabyte.tdl
4e5dbe2
rename to tests/tdl/test-58-disk-size-tebibyte-compat.tdl
4e5dbe2
diff --git a/tests/tdl/test-63-disk-size-exbibyte.tdl b/tests/tdl/test-63-disk-size-exbibyte.tdl
4e5dbe2
new file mode 100644
4e5dbe2
index 0000000..e3f6b12
4e5dbe2
--- /dev/null
4e5dbe2
+++ b/tests/tdl/test-63-disk-size-exbibyte.tdl
4e5dbe2
@@ -0,0 +1,15 @@
4e5dbe2
+<template>
4e5dbe2
+  <name>help</name>
4e5dbe2
+  <os>
4e5dbe2
+    <name>Fedora</name>
4e5dbe2
+    <version>12</version>
4e5dbe2
+    <arch>i386</arch>
4e5dbe2
+    <install type='url'>
4e5dbe2
+      <url>http://download.fedoraproject.org/pub/fedora/linux/releases/12/Fedora/x86_64/os/</url>
4e5dbe2
+    </install>
4e5dbe2
+  </os>
4e5dbe2
+  <description>My Fedora 12 JEOS image</description>
4e5dbe2
+  <disk>
4e5dbe2
+    <size>2EiB</size>
4e5dbe2
+  </disk>
4e5dbe2
+</template>
4e5dbe2
diff --git a/tests/tdl/test-64-disk-size-zettabyte.tdl b/tests/tdl/test-64-disk-size-zettabyte.tdl
4e5dbe2
new file mode 100644
4e5dbe2
index 0000000..26b7105
4e5dbe2
--- /dev/null
4e5dbe2
+++ b/tests/tdl/test-64-disk-size-zettabyte.tdl
4e5dbe2
@@ -0,0 +1,15 @@
4e5dbe2
+<template>
4e5dbe2
+  <name>help</name>
4e5dbe2
+  <os>
4e5dbe2
+    <name>Fedora</name>
4e5dbe2
+    <version>12</version>
4e5dbe2
+    <arch>i386</arch>
4e5dbe2
+    <install type='url'>
4e5dbe2
+      <url>http://download.fedoraproject.org/pub/fedora/linux/releases/12/Fedora/x86_64/os/</url>
4e5dbe2
+    </install>
4e5dbe2
+  </os>
4e5dbe2
+  <description>My Fedora 12 JEOS image</description>
4e5dbe2
+  <disk>
4e5dbe2
+    <size>10ZB</size>
4e5dbe2
+  </disk>
4e5dbe2
+</template>
4e5dbe2
diff --git a/tests/tdl/test-65-disk-size-byte.tdl b/tests/tdl/test-65-disk-size-byte.tdl
4e5dbe2
new file mode 100644
4e5dbe2
index 0000000..b6e0774
4e5dbe2
--- /dev/null
4e5dbe2
+++ b/tests/tdl/test-65-disk-size-byte.tdl
4e5dbe2
@@ -0,0 +1,15 @@
4e5dbe2
+<template>
4e5dbe2
+  <name>help</name>
4e5dbe2
+  <os>
4e5dbe2
+    <name>Fedora</name>
4e5dbe2
+    <version>12</version>
4e5dbe2
+    <arch>i386</arch>
4e5dbe2
+    <install type='url'>
4e5dbe2
+      <url>http://download.fedoraproject.org/pub/fedora/linux/releases/12/Fedora/x86_64/os/</url>
4e5dbe2
+    </install>
4e5dbe2
+  </os>
4e5dbe2
+  <description>My Fedora 12 JEOS image</description>
4e5dbe2
+  <disk>
4e5dbe2
+    <size>10000000 B</size>
4e5dbe2
+  </disk>
4e5dbe2
+</template>
4e5dbe2
diff --git a/tests/tdl/test_tdl.py b/tests/tdl/test_tdl.py
4e5dbe2
index a94b2c1..366f1bc 100644
4e5dbe2
--- a/tests/tdl/test_tdl.py
4e5dbe2
+++ b/tests/tdl/test_tdl.py
4e5dbe2
@@ -92,8 +92,11 @@ tests = {
4e5dbe2
     "test-55-files-http-url.tdl": True,
4e5dbe2
     "test-56-invalid-disk-size.tdl": False,
4e5dbe2
     "test-57-invalid-disk-size.tdl": False,
4e5dbe2
-    "test-58-disk-size-terabyte.tdl": True,
4e5dbe2
+    "test-58-disk-size-tebibyte-compat.tdl": True,
4e5dbe2
     "test-59-command-sorting.tdl": True,
4e5dbe2
+    "test-63-disk-size-exbibyte.tdl": True,
4e5dbe2
+    "test-64-disk-size-zettabyte.tdl": True,
4e5dbe2
+    "test-65-disk-size-byte.tdl": True,
4e5dbe2
 }
4e5dbe2
 
4e5dbe2
 # Test that iterates over all .tdl files
4e5dbe2
-- 
4e5dbe2
2.43.0
4e5dbe2