lib: image_lib: switch from partx to direct BLKPG ioctls

The partx tool has some annoying behaviors that are hard to workaround:
* You can't safely call `partx -d` as it errors out when all of the
  partitions have already been removed.
* You can't safely call `partx -a` as it errors out when any of the
  partitions already exist.
* `partx -a` sometimes fails with unhelpful errors like "error adding
  partitions".
* `partx -u` is really a `partx -d && partx -a` rather than a real
  "update missing partitions", so trying to use it to recover from a
  flake is actually hoping you don't flake twice in a row.
* Because the steps are so coarse, trying to put retries around any of
  these steps is basically.

For these reasons, take over adding & removing the partitions via the
BLKPG ioctl calls ourselves.  This will allow us to see the real error
coming from the kernel, and to retry & update the specific parts that
are missing, or poll on the specific parts that the kernel hasn't yet
created.

This means we have to understand the disk image partition table, but
we already have to parse it, and have all the support code to do so.

BUG=b:256896261
TEST=CQ passes

Change-Id: I0166f7782ba21acd7c8a9371e4a4e7f098d7ae0f
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/4130559
Reviewed-by: Ram Chandrasekar <rchandrasekar@google.com>
Commit-Queue: Mike Frysinger <vapier@chromium.org>
Tested-by: Mike Frysinger <vapier@chromium.org>
diff --git a/scripts/test_image_unittest.py b/scripts/test_image_unittest.py
index 9007952..6907549 100644
--- a/scripts/test_image_unittest.py
+++ b/scripts/test_image_unittest.py
@@ -50,6 +50,7 @@
         self.PatchObject(
             image_lib.LoopbackPartitions, "_Unmount", autospec=True
         )
+        self.PatchObject(image_lib.LoopbackPartitions, "Attach", autospec=True)
 
 
 class FindImageTest(TestImageTest):