Repository: pulp/pulpcore

Issue:
Division by zero during Pulp import
**Version**
3.36.0+

**Describe the bug**

During a Katello test run, the following exception was encountered

```
 {"traceback"=>"  File \"/usr/lib/python3.11/site-packages/pulpcore/tasking/tasks.py\", line 61, in _execute_task\n    result = func(*args, **kwargs)\n             ^^^^^^^^^^^^^^^^^^^^^\n  File \"/usr/lib/python3.11/site-packages/pulpcore/app/tasks/importer.py\", line 453, in pulp_import\n    with tarfile.open(path, \"r\", fileobj=fp) as tar:\n         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/usr/lib64/python3.11/tarfile.py\", line 1815, in open\n    fileobj.seek(saved_pos)\n  File \"/usr/lib/python3.11/site-packages/pulpcore/app/tasks/importer.py\", line 132, in seek\n    self.chunk = target // self.chunk_size\n                 ~~~~~~~^^~~~~~~~~~~~~~~~~\n", "description"=>"integer division or modulo by zero"} (Katello::Errors::Pulp3Error)
```

I'm not 100% certain, but I suspect the cause here is that if chunks aren't used during the export, chunk_size is set to 0:

https://github.com/pulp/pulpcore/blob/main/pulpcore/app/tasks/export.py#L456C31-L456C31

and ChunkedFile reads that value here:

https://github.com/pulp/pulpcore/blob/main/pulpcore/app/tasks/importer.py#L79

and we're using ChunkedFile even in the non-chunked case so long as a TOC file was provided.

https://github.com/pulp/pulpcore/blob/main/pulpcore/app/tasks/importer.py#L335-L336

**To Reproduce**

I would expect it to be reproducible if performing a non-chunked export, and providing a TOC path for the import.

**Expected behavior**

We should never see an integer division by zero error.

**Additional context**




Discussion:
We'll also want to expand our tests to cover this
Does this mean that we can provide a toc file that makes a non-chunked import look like one with a single chunk, only the specified chunk-size is wrong?

Resolving patch:
diff --git a/pulpcore/app/serializers/exporter.py b/pulpcore/app/serializers/exporter.py
--- a/pulpcore/app/serializers/exporter.py
+++ b/pulpcore/app/serializers/exporter.py
@@ -1,6 +1,6 @@
 import os
-import re
 from gettext import gettext as _
+import re
 
 from rest_framework import serializers
 from rest_framework.validators import UniqueValidator
@@ -19,6 +19,16 @@
 from pulpcore.constants import FS_EXPORT_CHOICES, FS_EXPORT_METHODS
 
 
+def parse_human_readable_file_size(size: str):
+    # based on https://stackoverflow.com/a/42865957/2002471
+    units = {"B": 1, "KB": 2**10, "MB": 2**20, "GB": 2**30, "TB": 2**40}
+    size = size.upper()
+    if not re.match(r" ", size):
+        size = re.sub(r"([KMGT]?B)", r" \1", size)
+    number, unit = [string.strip() for string in size.split()]
+    return int(float(number) * units[unit])
+
+
 class ExporterSerializer(ModelSerializer):
     """
     Base serializer for Exporters.
@@ -208,23 +218,13 @@ def validate(self, data):
                 )
         return super().validate(data)
 
-    @staticmethod
-    def _parse_size(size):
+    def validate_chunk_size(self, chunk_size):
         try:
-            # based on https://stackoverflow.com/a/42865957/2002471
-            units = {"B": 1, "KB": 2**10, "MB": 2**20, "GB": 2**30, "TB": 2**40}
-            size = size.upper()
-            if not re.match(r" ", size):
-                size = re.sub(r"([KMGT]?B)", r" \1", size)
-            number, unit = [string.strip() for string in size.split()]
-            return int(float(number) * units[unit])
+            the_size = parse_human_readable_file_size(chunk_size)
         except ValueError:
             raise serializers.ValidationError(
-                _("chunk_size '{}' is not valid (valid units are B/KB/MB/GB/TB)").format(size)
+                _("chunk_size '{}' is not valid (valid units are B/KB/MB/GB/TB)").format(chunk_size)
             )
-
-    def validate_chunk_size(self, chunk_size):
-        the_size = self._parse_size(chunk_size)
         if the_size <= 0:
             raise serializers.ValidationError(
                 _("Chunk size {} is not greater than zero!").format(the_size)
diff --git a/pulpcore/app/tasks/export.py b/pulpcore/app/tasks/export.py
--- a/pulpcore/app/tasks/export.py
+++ b/pulpcore/app/tasks/export.py
@@ -422,12 +422,10 @@ def pulp_export(exporter_pk, params):
                         os.remove(pathname)
                     raise
             # compute the hashes
-            global_hash = hasher()
             paths = sorted([str(Path(p)) for p in glob(tarfile_fp + ".*")])
             for a_file in paths:
-                a_hash = compute_file_hash(a_file, hasher=hasher(), cumulative_hash=global_hash)
+                a_hash = compute_file_hash(a_file, hasher=hasher())
                 rslts[a_file] = a_hash
-            tarfile_hash = global_hash.hexdigest()
 
         else:
             # write into the file
@@ -450,23 +448,20 @@ def pulp_export(exporter_pk, params):
         # write outputfile/hash info to a file 'next to' the output file(s)
         output_file_info_path = tarfile_fp.replace(".tar", "-toc.json")
         with open(output_file_info_path, "w") as outfile:
-            if the_export.validated_chunk_size:
-                chunk_size = the_export.validated_chunk_size
-            else:
-                chunk_size = 0
-            chunk_toc = {
+            table_of_contents = {
                 "meta": {
-                    "chunk_size": chunk_size,
-                    "file": os.path.basename(tarfile_fp),
-                    "global_hash": tarfile_hash,
                     "checksum_type": checksum_type,
                 },
                 "files": {},
             }
+
+            if the_export.validated_chunk_size:
+                table_of_contents["meta"]["chunk_size"] = the_export.validated_chunk_size
+
             # Build a toc with just filenames (not the path on the exporter-machine)
             for a_path in rslts.keys():
-                chunk_toc["files"][os.path.basename(a_path)] = rslts[a_path]
-            json.dump(chunk_toc, outfile)
+                table_of_contents["files"][os.path.basename(a_path)] = rslts[a_path]
+            json.dump(table_of_contents, outfile)
 
         # store toc info
         toc_hash = compute_file_hash(output_file_info_path)
diff --git a/pulpcore/app/tasks/importer.py b/pulpcore/app/tasks/importer.py
--- a/pulpcore/app/tasks/importer.py
+++ b/pulpcore/app/tasks/importer.py
@@ -76,12 +76,17 @@ def __init__(self, toc_path):
             raise ValidationError(_("Missing 'files' or 'meta' keys in table-of-contents!"))
 
         toc_dir = os.path.dirname(toc_path)
-        self.chunk_size = int(self.toc["meta"]["chunk_size"])
         # sorting-by-filename is REALLY IMPORTANT here
         # keys are of the form <base-export-name>.00..<base-export-name>.NN,
         # and must be reassembled IN ORDER
         self.chunk_names = sorted(self.toc["files"].keys())
         self.chunk_paths = [os.path.join(toc_dir, chunk_name) for chunk_name in self.chunk_names]
+        self.chunk_size = int(self.toc["meta"].get("chunk_size", 0))
+        if not self.chunk_size:
+            assert (
+                len(self.toc["files"]) == 1
+            ), "chunk_size must exist and be non-zero if more than one chunk exists"
+            self.chunk_size = os.path.getsize(self.chunk_paths[0])
 
     def __enter__(self):
         assert not hasattr(self, "chunks"), "ChunkedFile is not reentrant."


Test patch: