ClusterAPI.imageBuilder/hack/image-build-ova.py

273 lines
11 KiB
Python
Raw Normal View History

2023-02-22 20:24:42 +00:00
#!/usr/bin/env python3
# Copyright 2019 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
################################################################################
# usage: image-build-ova.py [FLAGS] ARGS
# This program builds an OVA file from a VMDK and manifest file generated as a
# result of a Packer build.
################################################################################
import argparse
import hashlib
import io
import json
import os
import subprocess
from string import Template
import tarfile
def main():
parser = argparse.ArgumentParser(
description="Builds an OVA using the artifacts from a Packer build")
parser.add_argument('--stream_vmdk',
dest='stream_vmdk',
action='store_true',
help='Compress vmdk file')
parser.add_argument('--vmx',
dest='vmx_version',
default='15',
help='The virtual hardware version')
parser.add_argument('--eula_file',
nargs='?',
metavar='EULA',
default='./ovf_eula.txt',
help='Text file containing EULA')
parser.add_argument('--ovf_template',
nargs='?',
metavar='OVF_TEMPLATE',
default='./ovf_template.xml',
help='XML template to build OVF')
parser.add_argument('--vmdk_file',
nargs='?',
metavar='FILE',
default=None,
help='Use FILE as VMDK instead of reading from manifest. '
'Must be in BUILD_DIR')
parser.add_argument(dest='build_dir',
nargs='?',
metavar='BUILD_DIR',
default='.',
help='The Packer build directory')
args = parser.parse_args()
# Read in the EULA
eula = ""
with io.open(args.eula_file, 'r', encoding='utf-8') as f:
eula = f.read()
# Read in the OVF template
ovf_template = ""
with io.open(args.ovf_template, 'r', encoding='utf-8') as f:
ovf_template = f.read()
# Change the working directory if one is specified.
os.chdir(args.build_dir)
print("image-build-ova: cd %s" % args.build_dir)
# Load the packer manifest JSON
data = None
with open('packer-manifest.json', 'r') as f:
data = json.load(f)
# Get the first build.
build = data['builds'][0]
build_data = build['custom_data']
print("image-build-ova: loaded %s-kube-%s" % (build_data['build_name'],
build_data['kubernetes_semver']))
if args.vmdk_file is None:
# Get a list of the VMDK files from the packer manifest.
vmdk_files = get_vmdk_files(build['files'])
else:
vmdk_files = [{"name": args.vmdk_file, "size": os.path.getsize(args.vmdk_file)}]
# Create stream-optimized versions of the VMDK files.
if args.stream_vmdk is True:
stream_optimize_vmdk_files(vmdk_files)
else:
for f in vmdk_files:
f['stream_name'] = f['name']
f['stream_size'] = os.path.getsize(f['name'])
# TODO(akutz) Support multiple VMDK files in the OVF/OVA
vmdk = vmdk_files[0]
OS_id_map = {"vmware-photon-64": {"id": "36", "version": "", "type": "vmwarePhoton64Guest"},
"centos7-64": {"id": "107", "version": "7", "type": "centos7-64"},
"centos8-64": {"id": "107", "version": "8", "type": "centos8-64"},
"rhel7-64": {"id": "80", "version": "7", "type": "rhel7_64guest"},
"rhel8-64": {"id": "80", "version": "8", "type": "rhel8_64guest"},
"ubuntu-64": {"id": "94", "version": "", "type": "ubuntu64Guest"},
"flatcar-64": {"id": "100", "version": "", "type": "linux-64"},
"Windows2019Server-64": {"id": "112", "version": "", "type": "windows9srv-64"},
"Windows2004Server-64": {"id": "112", "version": "", "type": "windows9srv-64"}}
# Create the OVF file.
data = {
'BUILD_DATE': build_data['build_date'],
'ARTIFACT_ID': build['artifact_id'],
'BUILD_TIMESTAMP': build_data['build_timestamp'],
'EULA': eula,
'OS_NAME': build_data['os_name'],
'OS_ID': OS_id_map[build_data['guest_os_type']]['id'],
'OS_TYPE': OS_id_map[build_data['guest_os_type']]['type'],
'OS_VERSION': OS_id_map[build_data['guest_os_type']]['version'],
'IB_VERSION': build_data['ib_version'],
'DISK_NAME': vmdk['stream_name'],
'DISK_SIZE': build_data['disk_size'],
'POPULATED_DISK_SIZE': vmdk['size'],
'STREAM_DISK_SIZE': vmdk['stream_size'],
'VMX_VERSION': args.vmx_version,
'DISTRO_NAME': build_data['distro_name'],
'DISTRO_VERSION': build_data['distro_version'],
'DISTRO_ARCH': build_data['distro_arch'],
'NESTEDHV': "false",
'FIRMWARE': build_data['firmware']
}
capv_url = "https://github.com/kubernetes-sigs/cluster-api-provider-vsphere"
data['CNI_VERSION'] = build_data['kubernetes_cni_semver']
data['CONTAINERD_VERSION'] = build_data['containerd_version']
data['KUBERNETES_SEMVER'] = build_data['kubernetes_semver']
data['KUBERNETES_SOURCE_TYPE'] = build_data['kubernetes_source_type']
data['PRODUCT'] = "%s and Kubernetes %s" % (
build_data['os_name'], build_data['kubernetes_semver'])
data['ANNOTATION'] = "Cluster API vSphere image - %s - %s" % (data['PRODUCT'], capv_url)
data['WAKEONLANENABLED'] = "false"
data['TYPED_VERSION'] = build_data['kubernetes_typed_version']
data['PROPERTIES'] = Template('''
<Property ovf:userConfigurable="false" ovf:value="${DISTRO_NAME}" ovf:type="string" ovf:key="DISTRO_NAME"/>
<Property ovf:userConfigurable="false" ovf:value="${DISTRO_VERSION}" ovf:type="string" ovf:key="DISTRO_VERSION"/>
<Property ovf:userConfigurable="false" ovf:value="${DISTRO_ARCH}" ovf:type="string" ovf:key="DISTRO_ARCH"/>
<Property ovf:userConfigurable="false" ovf:value="${CNI_VERSION}" ovf:type="string" ovf:key="CNI_VERSION"/>
<Property ovf:userConfigurable="false" ovf:value="${CONTAINERD_VERSION}" ovf:type="string" ovf:key="CONTAINERD_VERSION"/>
<Property ovf:userConfigurable="false" ovf:value="${KUBERNETES_SEMVER}" ovf:type="string" ovf:key="KUBERNETES_SEMVER"/>
<Property ovf:userConfigurable="false" ovf:value="${KUBERNETES_SOURCE_TYPE}" ovf:type="string" ovf:key="KUBERNETES_SOURCE_TYPE"/>\n''').substitute(data)
# Check if OVF_CUSTOM_PROPERTIES environment Variable is set.
# If so, load the json file & add the properties to the OVF
if os.environ.get("OVF_CUSTOM_PROPERTIES"):
with open(os.environ.get("OVF_CUSTOM_PROPERTIES"), 'r') as f:
custom_properties = json.loads(f.read())
if custom_properties:
for k, v in custom_properties.items():
data['PROPERTIES'] = data['PROPERTIES'] + \
f''' <Property ovf:userConfigurable="false" ovf:value="{v}" ovf:type="string" ovf:key="{k}"/>\n'''
if "windows" in OS_id_map[build_data['guest_os_type']]['type']:
if build_data['disable_hypervisor'] != "true":
data['NESTEDHV'] = "true"
ovf = "%s-%s.ovf" % (build_data['build_name'], data['TYPED_VERSION'])
mf = "%s-%s.mf" % (build_data['build_name'], data['TYPED_VERSION'])
ova = "%s-%s.ova" % (build_data['build_name'], data['TYPED_VERSION'])
# Create OVF
create_ovf(ovf, data, ovf_template)
if os.environ.get("IB_OVFTOOL"):
# Create the OVA.
create_ova(ova, ovf, ovftool_args=os.environ.get("IB_OVFTOOL_ARGS", ""))
else:
# Create the OVA manifest.
create_ova_manifest(mf, [ovf, vmdk['stream_name']])
# Create the OVA
create_ova(ova, ovf, ova_files=[mf, vmdk['stream_name']])
def sha256(path):
m = hashlib.sha256()
with open(path, 'rb') as f:
while True:
data = f.read(65536)
if not data:
break
m.update(data)
return m.hexdigest()
def create_ova(ova_path, ovf_path, ovftool_args=None, ova_files=None):
if ova_files is None:
cmd = f"ovftool {ovftool_args} {ovf_path} {ova_path}"
print("image-build-ova: creating OVA from %s using ovftool" %
ovf_path)
subprocess.run(cmd.split(), check=True)
else:
infile_paths = [ovf_path]
infile_paths.extend(ova_files)
print("image-build-ova: creating OVA using tar")
with open(ova_path, 'wb') as f:
with tarfile.open(fileobj=f, mode='w|') as tar:
for infile_path in infile_paths:
tar.add(infile_path)
chksum_path = "%s.sha256" % ova_path
print("image-build-ova: create ova checksum %s" % chksum_path)
with open(chksum_path, 'w') as f:
f.write(sha256(ova_path))
def create_ovf(path, data, ovf_template):
print("image-build-ova: create ovf %s" % path)
with io.open(path, 'w', encoding='utf-8') as f:
f.write(Template(ovf_template).substitute(data))
def create_ova_manifest(path, infile_paths):
print("image-build-ova: create ova manifest %s" % path)
with open(path, 'w') as f:
for i in infile_paths:
f.write('SHA256(%s)= %s\n' % (i, sha256(i)))
def get_vmdk_files(inlist):
outlist = []
for f in inlist:
if f['name'].endswith('.vmdk'):
outlist.append(f)
return outlist
def stream_optimize_vmdk_files(inlist):
for f in inlist:
infile = f['name']
outfile = infile.replace('.vmdk', '.ova.vmdk', 1)
if os.path.isfile(outfile):
os.remove(outfile)
args = [
'vmware-vdiskmanager',
'-r', infile,
'-t', '5',
outfile
]
print("image-build-ova: stream optimize %s --> %s (1-2 minutes)" %
(infile, outfile))
subprocess.check_call(args)
f['stream_name'] = outfile
f['stream_size'] = os.path.getsize(outfile)
if __name__ == "__main__":
main()