mirror of https://github.com/ipxe/ipxe.git
168 lines
5.8 KiB
Python
168 lines
5.8 KiB
Python
#!/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))
|