[cloud] Add utility to read INT13CON partition in Google Compute Engine

Following the example of aws-int13con, add a utility that can be used
to read the INT13 console log from a used iPXE boot disk in Google
Compute Engine.

There seems to be no easy way to directly read the contents of either
a disk image or a snapshot in Google Cloud.  Work around this
limitation by creating a snapshot and attaching this snapshot as a
data disk to a temporary Linux instance, which is then used to echo
the INT13 console log to the serial port.

Signed-off-by: Michael Brown <mcb30@ipxe.org>
diff --git a/contrib/cloud/gce-int13con b/contrib/cloud/gce-int13con
new file mode 100755
index 0000000..3b909a4
--- /dev/null
+++ b/contrib/cloud/gce-int13con
@@ -0,0 +1,146 @@
+#!/usr/bin/env python3
+
+import argparse
+import textwrap
+import time
+from uuid import uuid4
+
+from google.cloud import compute
+
+IPXE_LOG_PREFIX = 'ipxe-log-temp-'
+IPXE_LOG_MAGIC = 'iPXE LOG'
+IPXE_LOG_END = '----- END OF iPXE LOG -----'
+
+def get_log_disk(instances, project, zone, name):
+    """Get log disk source URL"""
+    instance = instances.get(project=project, zone=zone, instance=name)
+    disk = next(x for x in instance.disks if x.boot)
+    return disk.source
+
+def delete_temp_snapshot(snapshots, project, name):
+    """Delete temporary snapshot"""
+    assert name.startswith(IPXE_LOG_PREFIX)
+    snapshots.delete(project=project, snapshot=name)
+
+def delete_temp_snapshots(snapshots, project):
+    """Delete all old temporary snapshots"""
+    filter = "name eq %s.+" % IPXE_LOG_PREFIX
+    request = compute.ListSnapshotsRequest(project=project, filter=filter)
+    for snapshot in snapshots.list(request=request):
+        delete_temp_snapshot(snapshots, project, snapshot.name)
+
+def create_temp_snapshot(snapshots, project, source):
+    """Create temporary snapshot"""
+    name = '%s%s' % (IPXE_LOG_PREFIX, uuid4())
+    snapshot = compute.Snapshot(name=name, source_disk=source)
+    snapshots.insert(project=project, snapshot_resource=snapshot).result()
+    return name
+
+def delete_temp_instance(instances, project, zone, name):
+    """Delete log dumper temporary instance"""
+    assert name.startswith(IPXE_LOG_PREFIX)
+    instances.delete(project=project, zone=zone, instance=name)
+
+def delete_temp_instances(instances, project, zone):
+    """Delete all old log dumper temporary instances"""
+    filter = "name eq %s.+" % IPXE_LOG_PREFIX
+    request = compute.ListInstancesRequest(project=project, zone=zone,
+                                           filter=filter)
+    for instance in instances.list(request=request):
+        delete_temp_instance(instances, project, zone, instance.name)
+
+def create_temp_instance(instances, project, zone, family, image, machine,
+                         snapshot):
+    """Create log dumper temporary instance"""
+    image = "projects/%s/global/images/family/%s" % (family, image)
+    machine_type = "zones/%s/machineTypes/%s" % (zone, machine)
+    logsource = "global/snapshots/%s" % snapshot
+    bootparams = compute.AttachedDiskInitializeParams(source_image=image)
+    bootdisk = compute.AttachedDisk(boot=True, auto_delete=True,
+                                    initialize_params=bootparams)
+    logparams = compute.AttachedDiskInitializeParams(source_snapshot=logsource)
+    logdisk = compute.AttachedDisk(boot=False, auto_delete=True,
+                                   initialize_params=logparams,
+                                   device_name="ipxelog")
+    nic = compute.NetworkInterface()
+    name = '%s%s' % (IPXE_LOG_PREFIX, uuid4())
+    script = textwrap.dedent(f"""
+    #!/bin/sh
+    tr -d '\\000' < /dev/disk/by-id/google-ipxelog-part3 > /dev/ttyS3
+    echo "{IPXE_LOG_END}" > /dev/ttyS3
+    """).strip()
+    items = compute.Items(key="startup-script", value=script)
+    metadata = compute.Metadata(items=[items])
+    instance = compute.Instance(name=name, machine_type=machine_type,
+                                network_interfaces=[nic], metadata=metadata,
+                                disks=[bootdisk, logdisk])
+    instances.insert(project=project, zone=zone,
+                     instance_resource=instance).result()
+    return name
+
+def get_log_output(instances, project, zone, name):
+    """Get iPXE log output"""
+    request = compute.GetSerialPortOutputInstanceRequest(project=project,
+                                                         zone=zone, port=4,
+                                                         instance=name)
+    while True:
+        log = instances.get_serial_port_output(request=request).contents.strip()
+        if log.endswith(IPXE_LOG_END):
+            if log.startswith(IPXE_LOG_MAGIC):
+                return log[len(IPXE_LOG_MAGIC):-len(IPXE_LOG_END)]
+            else:
+                return log[:-len(IPXE_LOG_END)]
+        time.sleep(1)
+
+# Parse command-line arguments
+#
+parser = argparse.ArgumentParser(description="Import Google Cloud image")
+parser.add_argument('--project', '-j', default="ipxe-images",
+                    help="Google Cloud project")
+parser.add_argument('--zone', '-z', required=True,
+                    help="Google Cloud zone")
+parser.add_argument('--family', '-f', default="debian-cloud",
+                    help="Helper OS image family")
+parser.add_argument('--image', '-i', default="debian-12",
+                    help="Helper OS image")
+parser.add_argument('--machine', '-m', default="e2-micro",
+                    help="Helper machine type")
+parser.add_argument('instance', help="Instance name")
+args = parser.parse_args()
+
+# Construct client objects
+#
+instances = compute.InstancesClient()
+snapshots = compute.SnapshotsClient()
+
+# Clean up old temporary objects
+#
+delete_temp_instances(instances, project=args.project, zone=args.zone)
+delete_temp_snapshots(snapshots, project=args.project)
+
+# Create log disk snapshot
+#
+logdisk = get_log_disk(instances, project=args.project, zone=args.zone,
+                       name=args.instance)
+logsnap = create_temp_snapshot(snapshots, project=args.project, source=logdisk)
+
+# Create log dumper instance
+#
+dumper = create_temp_instance(instances, project=args.project, zone=args.zone,
+                              family=args.family, image=args.image,
+                              machine=args.machine, snapshot=logsnap)
+
+# Wait for log output
+#
+output = get_log_output(instances, project=args.project, zone=args.zone,
+                        name=dumper)
+
+# Print log output
+#
+print(output)
+
+# Clean up
+#
+delete_temp_instance(instances, project=args.project, zone=args.zone,
+                     name=dumper)
+delete_temp_snapshot(snapshots, project=args.project, name=logsnap)