aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xcontrib/cloud/gce-import167
1 files changed, 167 insertions, 0 deletions
diff --git a/contrib/cloud/gce-import b/contrib/cloud/gce-import
new file mode 100755
index 0000000..e7adfee
--- /dev/null
+++ b/contrib/cloud/gce-import
@@ -0,0 +1,167 @@
+#!/usr/bin/env python3
+
+import argparse
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from datetime import date
+import io
+import subprocess
+import tarfile
+from uuid import uuid4
+
+from google.cloud import compute
+from google.cloud import exceptions
+from google.cloud import storage
+
+IPXE_STORAGE_PREFIX = 'ipxe-upload-temp-'
+
+FEATURE_GVNIC = compute.GuestOsFeature(type_="GVNIC")
+FEATURE_IDPF = compute.GuestOsFeature(type_="IDPF")
+FEATURE_UEFI = compute.GuestOsFeature(type_="UEFI_COMPATIBLE")
+
+POLICY_PUBLIC = compute.Policy(bindings=[{
+ "role": "roles/compute.imageUser",
+ "members": ["allAuthenticatedUsers"],
+}])
+
+def delete_temp_bucket(bucket):
+ """Remove temporary bucket"""
+ assert bucket.name.startswith(IPXE_STORAGE_PREFIX)
+ for blob in bucket.list_blobs(prefix=IPXE_STORAGE_PREFIX):
+ assert blob.name.startswith(IPXE_STORAGE_PREFIX)
+ blob.delete()
+ if not list(bucket.list_blobs()):
+ bucket.delete()
+
+def create_temp_bucket(location):
+ """Create temporary bucket (and remove any stale temporary buckets)"""
+ client = storage.Client()
+ for bucket in client.list_buckets(prefix=IPXE_STORAGE_PREFIX):
+ delete_temp_bucket(bucket)
+ name = '%s%s' % (IPXE_STORAGE_PREFIX, uuid4())
+ return client.create_bucket(name, location=location)
+
+def create_tarball(image):
+ """Create raw disk image tarball"""
+ tarball = io.BytesIO()
+ with tarfile.open(fileobj=tarball, mode='w:gz',
+ format=tarfile.GNU_FORMAT) as tar:
+ tar.add(image, arcname='disk.raw')
+ tarball.seek(0)
+ return tarball
+
+def upload_blob(bucket, image):
+ """Upload raw disk image blob"""
+ blob = bucket.blob('%s%s.tar.gz' % (IPXE_STORAGE_PREFIX, uuid4()))
+ tarball = create_tarball(image)
+ blob.upload_from_file(tarball)
+ return blob
+
+def detect_uefi(image):
+ """Identify UEFI CPU architecture(s)"""
+ mdir = subprocess.run(['mdir', '-b', '-i', image, '::/EFI/BOOT'],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ check=False)
+ mapping = {
+ b'BOOTX64.EFI': 'x86_64',
+ b'BOOTAA64.EFI': 'arm64',
+ }
+ uefi = [
+ arch
+ for filename, arch in mapping.items()
+ if filename in mdir.stdout
+ ]
+ return uefi
+
+def image_architecture(uefi):
+ """Get image architecture"""
+ return uefi[0] if len(uefi) == 1 else None if uefi else 'x86_64'
+
+def image_features(uefi):
+ """Get image feature list"""
+ features = [FEATURE_GVNIC, FEATURE_IDPF]
+ if uefi:
+ features.append(FEATURE_UEFI)
+ return features
+
+def image_name(base, uefi):
+ """Calculate image name or family name"""
+ suffix = ('-uefi-%s' % uefi[0].replace('_', '-') if len(uefi) == 1 else
+ '-uefi-multi' if uefi else '')
+ return '%s%s' % (base, suffix)
+
+def create_image(project, basename, basefamily, overwrite, public, bucket,
+ image):
+ """Create image"""
+ client = compute.ImagesClient()
+ uefi = detect_uefi(image)
+ architecture = image_architecture(uefi)
+ features = image_features(uefi)
+ name = image_name(basename, uefi)
+ family = image_name(basefamily, uefi)
+ if overwrite:
+ try:
+ client.delete(project=project, image=name).result()
+ except exceptions.NotFound:
+ pass
+ blob = upload_blob(bucket, image)
+ disk = compute.RawDisk(source=blob.public_url)
+ image = compute.Image(name=name, family=family, architecture=architecture,
+ guest_os_features=features, raw_disk=disk)
+ client.insert(project=project, image_resource=image).result()
+ if public:
+ request = compute.GlobalSetPolicyRequest(policy=POLICY_PUBLIC)
+ client.set_iam_policy(project=project, resource=name,
+ global_set_policy_request_resource=request)
+ image = client.get(project=project, image=name)
+ return image
+
+# Parse command-line arguments
+#
+parser = argparse.ArgumentParser(description="Import Google Cloud image")
+parser.add_argument('--name', '-n',
+ help="Base image name")
+parser.add_argument('--family', '-f',
+ help="Base family name")
+parser.add_argument('--public', '-p', action='store_true',
+ help="Make image public")
+parser.add_argument('--overwrite', action='store_true',
+ help="Overwrite any existing image with same name")
+parser.add_argument('--project', '-j', default="ipxe-images",
+ help="Google Cloud project")
+parser.add_argument('--location', '-l',
+ help="Google Cloud Storage initial location")
+parser.add_argument('image', nargs='+', help="iPXE disk image")
+args = parser.parse_args()
+
+# Use default family name if none specified
+if not args.family:
+ args.family = 'ipxe'
+
+# Use default name if none specified
+if not args.name:
+ args.name = '%s-%s' % (args.family, date.today().strftime('%Y%m%d'))
+
+# Create temporary upload bucket
+bucket = create_temp_bucket(args.location)
+
+# Use one thread per image to maximise parallelism
+with ThreadPoolExecutor(max_workers=len(args.image)) as executor:
+ futures = {executor.submit(create_image,
+ project=args.project,
+ basename=args.name,
+ basefamily=args.family,
+ overwrite=args.overwrite,
+ public=args.public,
+ bucket=bucket,
+ image=image): image
+ for image in args.image}
+ results = {futures[future]: future.result()
+ for future in as_completed(futures)}
+
+# Delete temporary upload bucket
+delete_temp_bucket(bucket)
+
+# Show created images
+for image in args.image:
+ result = results[image]
+ print("%s (%s) %s" % (result.name, result.family, result.status))