feat: make UEFI work for x86_64, KubeZero version bump to 1.24

This commit is contained in:
Stefan Reimer 2022-10-31 17:20:05 +01:00
parent 4bb4791a55
commit 8bff1d5b0f
29 changed files with 183 additions and 310 deletions

View File

@ -1,6 +1,6 @@
OVERLAY := $(shell pwd)/overlay OVERLAY := $(shell pwd)/overlay
ONLY := ONLY :=
FILTER := --only 3.16 $(ONLY) --skip aarch64 FILTER := --only 3.16 $(ONLY)
STEP := publish STEP := publish
all: build all: build

View File

@ -1,5 +1,7 @@
*~ *~
*.bak *.bak
*.swp *.swp
.DS_Store
.vscode/ .vscode/
/work/ /work/
releases*yaml

View File

@ -208,10 +208,6 @@ may not) be further partitioned, based on other factors.
The image's primary login user, set to `alpine`. The image's primary login user, set to `alpine`.
### `local_format` string
The local VM's disk image format, set to `qcow2`.
### `repos` map ### `repos` map
Defines the contents of the image's `/etc/apk/repositories` file. The map's Defines the contents of the image's `/etc/apk/repositories` file. The map's

View File

@ -29,6 +29,11 @@ variable "qemu" {
### Local Data ### Local Data
locals { locals {
# possible actions for the post-processor
actions = [
"build", "upload", "import", "publish", "release"
]
debug_arg = var.DEBUG == 0 ? "" : "--debug" debug_arg = var.DEBUG == 0 ? "" : "--debug"
broker_arg = var.USE_BROKER == 0 ? "" : "--use-broker" broker_arg = var.USE_BROKER == 0 ? "" : "--use-broker"
@ -102,8 +107,8 @@ build {
# results # results
output_directory = "work/images/${B.value.cloud}/${B.value.image_key}" output_directory = "work/images/${B.value.cloud}/${B.value.image_key}"
disk_size = B.value.size disk_size = B.value.size
format = B.value.local_format format = "qcow2"
vm_name = "image.${B.value.local_format}" vm_name = "image.qcow2"
} }
} }
@ -181,13 +186,13 @@ build {
# import and/or publish cloud images # import and/or publish cloud images
dynamic "post-processor" { dynamic "post-processor" {
for_each = { for b, c in local.configs: for_each = { for b, c in local.configs:
b => c if contains(c.actions, "import") || contains(c.actions, "publish") b => c if length(setintersection(c.actions, local.actions)) > 0
} }
iterator = B iterator = B
labels = ["shell-local"] labels = ["shell-local"]
content { content {
only = [ "qemu.${B.key}", "null.${B.key}" ] only = [ "qemu.${B.key}", "null.${B.key}" ]
inline = [ for action in ["import", "publish"]: inline = [ for action in local.actions:
"./cloud_helper.py ${action} ${local.debug_arg} ${local.broker_arg} ${B.key}" if contains(B.value.actions, action) "./cloud_helper.py ${action} ${local.debug_arg} ${local.broker_arg} ${B.key}" if contains(B.value.actions, action)
] ]
} }

View File

@ -48,7 +48,7 @@ from image_configs import ImageConfigManager
### Constants & Variables ### Constants & Variables
STEPS = ['configs', 'state', 'rollback', 'local', 'import', 'publish'] STEPS = ['configs', 'state', 'rollback', 'local', 'upload', 'import', 'publish', 'release']
LOGFORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' LOGFORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
WORK_CLEAN = {'bin', 'include', 'lib', 'pyvenv.cfg', '__pycache__'} WORK_CLEAN = {'bin', 'include', 'lib', 'pyvenv.cfg', '__pycache__'}
WORK_OVERLAYS = ['configs', 'scripts'] WORK_OVERLAYS = ['configs', 'scripts']
@ -198,8 +198,6 @@ def install_qemu_firmware():
firm_bin = os.path.join(firm_dir, f"uefi-{arch}.bin") firm_bin = os.path.join(firm_dir, f"uefi-{arch}.bin")
os.symlink(bin, firm_bin) os.symlink(bin, firm_bin)
log.info('Padding "%s" to 67108864 bytes', firm_bin)
subprocess.run(['truncate', '-s', '67108864', firm_bin])
### Command Line & Logging ### Command Line & Logging
@ -224,8 +222,8 @@ parser.add_argument(
default=[], help='only variants with dimension key(s)') default=[], help='only variants with dimension key(s)')
parser.add_argument( parser.add_argument(
'--revise', action='store_true', '--revise', action='store_true',
help='remove existing local/imported image, or bump revision and rebuild' help='remove existing local/uploaded/imported image, or bump revision and '
'if published') ' rebuild if published or released')
parser.add_argument( parser.add_argument(
'--use-broker', action='store_true', '--use-broker', action='store_true',
help='use the identity broker to get credentials') help='use the identity broker to get credentials')
@ -252,7 +250,9 @@ console.setFormatter(logfmt)
log.addHandler(console) log.addHandler(console)
log.debug(args) log.debug(args)
# TODO: rollback requires --revise if args.step == 'rollback':
log.warning('"rollback" step enables --revise option')
args.revise = True
# set up credential provider, if we're going to use it # set up credential provider, if we're going to use it
if args.use_broker: if args.use_broker:

View File

@ -38,7 +38,7 @@ from image_configs import ImageConfigManager
### Constants & Variables ### Constants & Variables
ACTIONS = ['import', 'publish'] ACTIONS = ['build', 'upload', 'import', 'publish', 'release']
LOGFORMAT = '%(name)s - %(levelname)s - %(message)s' LOGFORMAT = '%(name)s - %(levelname)s - %(message)s'
@ -78,13 +78,26 @@ yaml.explicit_start = True
for image_key in args.image_keys: for image_key in args.image_keys:
image_config = configs.get(image_key) image_config = configs.get(image_key)
if args.action == 'import': if args.action == 'build':
clouds.convert_image(image_config) image_config.convert_image()
elif args.action == 'upload':
# TODO: image_config.upload_image()
pass
elif args.action == 'import':
clouds.import_image(image_config) clouds.import_image(image_config)
#clouds.upload_image(image_config)
elif args.action == 'publish': elif args.action == 'publish':
# TODO: we should probably always ensure the directory exists
os.makedirs(image_config.local_dir, exist_ok=True) os.makedirs(image_config.local_dir, exist_ok=True)
# TODO: save artifacts to image_config itself
artifacts = clouds.publish_image(image_config) artifacts = clouds.publish_image(image_config)
yaml.dump(artifacts, image_config.artifacts_yaml) yaml.dump(artifacts, image_config.artifacts_yaml)
#clouds.release_image(image_config) # sha256, sign, metadata, put in place for downloading
elif args.action == 'release':
pass
# TODO: image_config.release_image() - configurable steps to take on remote host
# save per-image metadata
image_config.save_metadata(upload=(False if args.action =='build' else True))

View File

@ -1,6 +1,6 @@
# vim: ts=4 et: # vim: ts=4 et:
from . import aws, nocloud # , oci, gcp, azure from . import aws # , oci, gcp, azure
ADAPTERS = {} ADAPTERS = {}
@ -12,7 +12,7 @@ def register(*mods):
ADAPTERS[cloud] = p ADAPTERS[cloud] = p
register(aws, nocloud) # , oci, azure, gcp) register(aws) # , oci, azure, gcp)
# using a credential provider is optional, set across all adapters # using a credential provider is optional, set across all adapters
@ -32,25 +32,13 @@ def latest_build_image(config):
) )
def convert_image(config):
return ADAPTERS[config.cloud].convert_image(config)
def import_image(config): def import_image(config):
return ADAPTERS[config.cloud].import_image(config) return ADAPTERS[config.cloud].import_image(config)
def remove_image(config, image_id): def delete_image(config, image_id):
return ADAPTERS[config.cloud].remove_image(image_id) return ADAPTERS[config.cloud].delete_image(image_id)
def upload_image(config):
return ADAPTERS[config.cloud].upload_image(config)
def publish_image(config): def publish_image(config):
return ADAPTERS[config.cloud].publish_image(config) return ADAPTERS[config.cloud].publish_image(config)
def release_image(config):
return ADAPTERS[config.cloud].release_image(config)

View File

@ -7,7 +7,7 @@ import os
import time import time
from datetime import datetime from datetime import datetime
from subprocess import Popen, PIPE, run from subprocess import run
from .interfaces.adapter import CloudAdapterInterface from .interfaces.adapter import CloudAdapterInterface
from image_configs import Tags, DictObj from image_configs import Tags, DictObj
@ -110,7 +110,6 @@ class AWSCloudAdapter(CloudAdapterInterface):
# import an image # import an image
# NOTE: requires 'vmimport' role with read/write of <s3_bucket>.* and its objects # NOTE: requires 'vmimport' role with read/write of <s3_bucket>.* and its objects
def import_image(self, ic): def import_image(self, ic):
log = logging.getLogger('import') log = logging.getLogger('import')
description = ic.image_description description = ic.image_description
@ -120,6 +119,7 @@ class AWSCloudAdapter(CloudAdapterInterface):
ec2r = session.resource('ec2') ec2r = session.resource('ec2')
bucket_name = 'alpine-cloud-images.' + hashlib.sha1(os.urandom(40)).hexdigest() bucket_name = 'alpine-cloud-images.' + hashlib.sha1(os.urandom(40)).hexdigest()
bucket = s3r.Bucket(bucket_name) bucket = s3r.Bucket(bucket_name)
log.info('Creating S3 bucket %s', bucket.name) log.info('Creating S3 bucket %s', bucket.name)
bucket.create( bucket.create(
@ -192,7 +192,8 @@ class AWSCloudAdapter(CloudAdapterInterface):
Architecture=self.ARCH[ic.arch], Architecture=self.ARCH[ic.arch],
BlockDeviceMappings=[{ BlockDeviceMappings=[{
'DeviceName': '/dev/xvda', 'DeviceName': '/dev/xvda',
'Ebs': {'SnapshotId': snapshot_id, 'VolumeType': 'gp3'} 'Ebs': {'SnapshotId': snapshot_id,
'VolumeType': 'gp3'}
}], }],
Description=description, Description=description,
EnaSupport=True, EnaSupport=True,
@ -227,8 +228,8 @@ class AWSCloudAdapter(CloudAdapterInterface):
return self._image_info(image) return self._image_info(image)
# remove an (unpublished) image # delete an (unpublished) image
def remove_image(self, image_id): def delete_image(self, image_id):
log = logging.getLogger('build') log = logging.getLogger('build')
ec2r = self.session().resource('ec2') ec2r = self.session().resource('ec2')
image = ec2r.Image(image_id) image = ec2r.Image(image_id)

View File

@ -1,20 +1,10 @@
# vim: ts=4 et: # vim: ts=4 et:
import logging
from subprocess import Popen, PIPE
class CloudAdapterInterface: class CloudAdapterInterface:
CONVERT_CMD = {
'qcow2': ['ln', '-f'],
'vhd': ['qemu-img', 'convert', '-f', 'qcow2', '-O', 'vpc', '-o', 'force_size=on'],
}
def __init__(self, cloud, cred_provider=None): def __init__(self, cloud, cred_provider=None):
self._sdk = None self._sdk = None
self._sessions = {} self._sessions = {}
self._clients = {}
self.cloud = cloud self.cloud = cloud
self.cred_provider = cred_provider self.cred_provider = cred_provider
self._default_region = None self._default_region = None
@ -37,47 +27,14 @@ class CloudAdapterInterface:
def session(self, region=None): def session(self, region=None):
raise NotImplementedError raise NotImplementedError
def client(self, client, region=None):
raise NotImplementedError
# get information about the latest released image
def latest_build_image(self, project, image_key): def latest_build_image(self, project, image_key):
raise NotImplementedError raise NotImplementedError
# convert local QCOW2 to format appropriate for a cloud
def convert_image(self, ic):
log = logging.getLogger('import')
local_path = ic.local_path
image_path = ic.local_dir / ic.image_file
log.info('Converting %s to %s', image_path, image_path)
p = Popen(self.CONVERT_CMD[ic.image_format] + [ic.local_path, ic.image_path], stdout=PIPE, stdin=PIPE, encoding='utf8')
out, err = p.communicate()
if p.returncode:
log.error('Unable to convert %s to %s format (%s)', ic.local_path, ic.image_path, p.returncode)
log.error('EXIT: %d', p.returncode)
log.error('STDOUT:\n%s', out)
log.error('STDERR:\n%s', err)
raise RuntimeError
# import local image to cloud provider
def import_image(self, config): def import_image(self, config):
raise NotImplementedError raise NotImplementedError
# remove unpublished image from cloud provider def delete_image(self, config, image_id):
def remove_image(self, config, image_id):
raise NotImplementedError raise NotImplementedError
# upload cloud image for testing, if upload_path
def upload_image(self, config):
raise NotImplementedError
# TODO: implement here
# publish image to cloud provider regions
def publish_image(self, config): def publish_image(self, config):
raise NotImplementedError raise NotImplementedError
# generate image checksum, save metadata, sign image, make downloadable, if download_path
def release_image(self, config):
raise NotImplementedError
# TODO: implement here!

View File

@ -1,51 +0,0 @@
# NOTE: not meant to be executed directly
# vim: ts=4 et:
import logging
import hashlib
import os
import time
from datetime import datetime
from subprocess import Popen, PIPE, run
from .interfaces.adapter import CloudAdapterInterface
from image_configs import Tags, DictObj
# For NoCloud, this will mostly be a no-op.
class NoCloudAdapter(CloudAdapterInterface):
IMAGE_INFO = [
'revision', 'imported', 'import_id', 'import_region', 'published',
]
# get the latest imported image for a given build name
def latest_build_image(self, project, image_key):
# TODO: get info from permanently published image (if exists)
return None
# import an image
def import_image(self, ic):
# TODO: what exactly should be returned?
return DictObj({
'revision': ic.revision,
'imported': datetime.now(),
# 'import_id': '?',
})
# remove an (unpublished) image
def remove_image(self, image_id):
# TODO: remove image from temporary location
pass
# publish an image
def publish_image(self, ic):
# TODO: what exaclty should be returned? nocloud isn't launchabl.
return {
'generic?': 'url?'
}
def register(cloud, cred_provider=None):
return NoCloudAdapter(cloud, cred_provider)

View File

@ -34,14 +34,12 @@ Default {
size = 1G size = 1G
login = alpine login = alpine
local_format = qcow2
image_format = qcow2 image_format = qcow2
# these paths are subject to change, as image downloads are developed # these paths are subject to change, as image downloads are developed
upload_path = "ssh://dev.alpinelinux.org/~tomalok/public_html/alpine-cloud-images" storage_url = "ssh://tomalok@dev.alpinelinux.org/public_html/alpine-cloud-images/{v_version}/cloud/{cloud}"
download_path = "https://dl-cdn.alpinelinux.org/alpine" download_url = "https://dev.alpinelinux.org/~tomalok/alpine-cloud-images/{v_version}/cloud/{cloud}" # development
#download_path = "https://dev.alpinelinux.org/~tomalok/alpine-cloud-images" # development #download_url = "https://dl-cdn.alpinelinux.org/alpine/{v_version}/cloud/{cloud}"
remote_path = "{v_version}/cloud/{cloud}"
# image access # image access
access.PUBLIC = true access.PUBLIC = true
@ -72,8 +70,7 @@ Dimensions {
cloudinit { include required("bootstrap/cloudinit.conf") } cloudinit { include required("bootstrap/cloudinit.conf") }
} }
cloud { cloud {
aws { include required("cloud/aws.conf") } aws { include required("cloud/aws.conf") }
nocloud { include required("cloud/nocloud.conf") }
} }
} }

View File

@ -2,8 +2,5 @@
name = [x86_64] name = [x86_64]
arch_name = x86_64 arch_name = x86_64
# TODO: until we have a image metadata service, let's avoid UEFI
EXCLUDE = [uefi]
qemu.machine_type = null qemu.machine_type = null
qemu.args = null qemu.args = null

View File

@ -25,12 +25,6 @@ WHEN {
} }
} }
} }
nocloud {
# tiny-cloud's nocloud support is currently > 3.16
EXCLUDE = ["3.12", "3.13", "3.14", "3.15", "3.16"]
packages.tiny-cloud-nocloud = true
}
# azure.packages.tiny-cloud-azure = true # azure.packages.tiny-cloud-azure = true
# gcp.packages.tiny-cloud-gcp = true # gcp.packages.tiny-cloud-gcp = true
# oci.packages.tiny-cloud-oci = true # oci.packages.tiny-cloud-oci = true

View File

@ -13,6 +13,7 @@ initfs_features {
ena = true ena = true
nvme = true nvme = true
} }
ntp_server = 169.254.169.123 ntp_server = 169.254.169.123
access.PUBLIC = true access.PUBLIC = true
@ -34,4 +35,4 @@ WHEN {
} }
} }
} }
} }

View File

@ -1,5 +0,0 @@
# vim: ts=2 et:
cloud_name = NoCloud
image_format = qcow2
ntp_server = ""

View File

@ -1,6 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# vim: ts=4 et: # vim: ts=4 et:
# TODO: perhaps integrate into "./build release"
# Ensure we're using the Python virtual env with our installed dependencies # Ensure we're using the Python virtual env with our installed dependencies
import os import os
import sys import sys

View File

@ -1,109 +0,0 @@
#!/usr/bin/env python3
# vim: ts=4 et:
# Ensure we're using the Python virtual env with our installed dependencies
import os
import sys
import textwrap
NOTE = textwrap.dedent("""
NOTE: This is an old script, replaced by 'gen_mksite_releases.py' after
https://gitlab.alpinelinux.org/alpine/infra/alpine-mksite/-/merge_requests/52
is merged.
This script's output is compatible with the retired alpine-ec2-ami repo's
releases/alpine.yaml, in order to bridge the gap until
https://alpinelinux.org/cloud dynamically calls a published-image metadata
service. This script should only be run after the main 'build' script has
been used successfully to publish ALL images, and the STDOUT should be
committed to the https://gitlab.alpinelinux.org/alpine/infra/alpine-mksite
repo as 'cloud/releases-in.yaml'.
""")
sys.pycache_prefix = 'work/__pycache__'
if not os.path.exists('work'):
print('FATAL: Work directory does not exist.', file=sys.stderr)
print(NOTE, file=sys.stderr)
exit(1)
# Re-execute using the right virtual environment, if necessary.
venv_args = [os.path.join('work', 'bin', 'python3')] + sys.argv
if os.path.join(os.getcwd(), venv_args[0]) != sys.executable:
print("Re-executing with work environment's Python...\n", file=sys.stderr)
os.execv(venv_args[0], venv_args)
# We're now in the right Python environment
import argparse
import logging
from collections import defaultdict
from ruamel.yaml import YAML
import clouds
from image_configs import ImageConfigManager
### Constants & Variables
LOGFORMAT = '%(name)s - %(levelname)s - %(message)s'
### Functions
# allows us to set values deep within an object that might not be fully defined
def dictfactory():
return defaultdict(dictfactory)
# undo dictfactory() objects to normal objects
def undictfactory(o):
if isinstance(o, defaultdict):
o = {k: undictfactory(v) for k, v in o.items()}
return o
### Command Line & Logging
parser = argparse.ArgumentParser(description=NOTE)
parser.add_argument(
'--use-broker', action='store_true',
help='use the identity broker to get credentials')
parser.add_argument('--debug', action='store_true', help='enable debug output')
args = parser.parse_args()
log = logging.getLogger('gen_releases')
log.setLevel(logging.DEBUG if args.debug else logging.INFO)
console = logging.StreamHandler(sys.stderr)
console.setFormatter(logging.Formatter(LOGFORMAT))
log.addHandler(console)
log.debug(args)
# set up credential provider, if we're going to use it
if args.use_broker:
clouds.set_credential_provider()
# load build configs
configs = ImageConfigManager(
conf_path='work/configs/images.conf',
yaml_path='work/images.yaml',
log='gen_releases'
)
# make sure images.yaml is up-to-date with reality
configs.refresh_state('final')
yaml = YAML()
releases = dictfactory()
for i_key, i_cfg in configs.get().items():
if i_cfg.bootstrap != 'tiny':
continue
release = i_cfg.version if i_cfg.version == 'edge' else i_cfg.release
releases[release][i_key][i_cfg.tags.name] = dict(i_cfg.tags) | {
'creation_date': i_cfg.published,
'artifacts': i_cfg.artifacts,
}
yaml.dump(undictfactory(releases), sys.stdout)

View File

@ -1,8 +1,10 @@
# vim: ts=4 et: # vim: ts=4 et:
import hashlib
import itertools import itertools
import logging import logging
import mergedeep import mergedeep
import os
import pyhocon import pyhocon
import shutil import shutil
@ -10,6 +12,8 @@ from copy import deepcopy
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from ruamel.yaml import YAML from ruamel.yaml import YAML
from subprocess import Popen, PIPE
from urllib.parse import urlparse
import clouds import clouds
@ -48,7 +52,8 @@ class ImageConfigManager():
def _load_yaml(self): def _load_yaml(self):
self.log.info('Loading existing %s', self.yaml_path) self.log.info('Loading existing %s', self.yaml_path)
for key, config in self.yaml.load(self.yaml_path).items(): for key, config in self.yaml.load(self.yaml_path).items():
self._configs[key] = ImageConfig(key, config) self._configs[key] = ImageConfig(key, config, log=self.log, yaml=self.yaml)
# TODO: also pull in additional per-image metatdata from the build process?
# save resolved configs to YAML # save resolved configs to YAML
def _save_yaml(self): def _save_yaml(self):
@ -90,7 +95,9 @@ class ImageConfigManager():
{ {
'image_key': image_key, 'image_key': image_key,
'release': release 'release': release
} | dim_map } | dim_map,
log=self.log,
yaml=self.yaml
) )
# merge in the Default config # merge in the Default config
@ -178,7 +185,18 @@ class ImageConfigManager():
class ImageConfig(): class ImageConfig():
def __init__(self, config_key, obj={}): CONVERT_CMD = {
'qcow2': ['ln', '-f'],
'vhd': ['qemu-img', 'convert', '-f', 'qcow2', '-O', 'vpc', '-o', 'force_size=on'],
}
# these tags may-or-may-not exist at various times
OPTIONAL_TAGS = [
'built', 'uploaded', 'imported', 'import_id', 'import_region', 'published', 'released'
]
def __init__(self, config_key, obj={}, log=None, yaml=None):
self._log = log
self._yaml = yaml
self.config_key = str(config_key) self.config_key = str(config_key)
tags = obj.pop('tags', None) tags = obj.pop('tags', None)
self.__dict__ |= self._deep_dict(obj) self.__dict__ |= self._deep_dict(obj)
@ -186,6 +204,18 @@ class ImageConfig():
if tags: if tags:
self.tags = tags self.tags = tags
@classmethod
def to_yaml(cls, representer, node):
d = {}
for k in node.__dict__:
# don't serialize attributes starting with _
if k.startswith('_'):
continue
d[k] = node.__getattribute__(k)
return representer.represent_mapping('!ImageConfig', d)
@property @property
def v_version(self): def v_version(self):
return 'edge' if self.version == 'edge' else 'v' + self.version return 'edge' if self.version == 'edge' else 'v' + self.version
@ -196,11 +226,7 @@ class ImageConfig():
@property @property
def local_path(self): def local_path(self):
return self.local_dir / ('image.' + self.local_format) return self.local_dir / ('image.qcow2')
@property
def published_yaml(self):
return self.local_dir / 'published.yaml'
@property @property
def artifacts_yaml(self): def artifacts_yaml(self):
@ -223,14 +249,13 @@ class ImageConfig():
return self.local_dir / self.image_file return self.local_dir / self.image_file
@property @property
def upload_url(self): def image_metadata_file(self):
return '/'.join([self.upload_path, self.remote_path, self.image_file]).format(v_version=self.v_version, **self.__dict__) return '.'.join([self.image_name, 'yaml'])
@property @property
def download_url(self): def image_metadata_path(self):
return '/'.join([self.download_path, self.remote_path, self.image_file]).format(v_version=self.v_version, **self.__dict__) return self.local_dir / self.image_metadata_file
# TODO? region_url instead?
def region_url(self, region, image_id): def region_url(self, region, image_id):
return self.cloud_region_url.format(region=region, image_id=image_id, **self.__dict__) return self.cloud_region_url.format(region=region, image_id=image_id, **self.__dict__)
@ -255,9 +280,10 @@ class ImageConfig():
'version': self.version 'version': self.version
} }
# stuff that might not be there yet # stuff that might not be there yet
for k in ['imported', 'import_id', 'import_region', 'published']: for k in self.OPTIONAL_TAGS:
if self.__dict__.get(k, None): if self.__dict__.get(k, None):
t[k] = self.__dict__[k] t[k] = self.__dict__[k]
return Tags(t) return Tags(t)
# recursively convert a ConfigTree object to a dict object # recursively convert a ConfigTree object to a dict object
@ -395,8 +421,9 @@ class ImageConfig():
for m, v in self.__dict__[d].items() for m, v in self.__dict__[d].items()
))) )))
# TODO: this needs to be sorted out for 'upload' and 'release' steps
def refresh_state(self, step, revise=False): def refresh_state(self, step, revise=False):
log = logging.getLogger('build') log = self._log
actions = {} actions = {}
revision = 0 revision = 0
remote_image = clouds.latest_build_image(self) remote_image = clouds.latest_build_image(self)
@ -435,7 +462,7 @@ class ImageConfig():
'Would remove' if step_state else 'Removing', 'Would remove' if step_state else 'Removing',
remote_image.import_id) remote_image.import_id)
if not step_state: if not step_state:
clouds.remove_image(self, remote_image.import_id) clouds.delete_image(self, remote_image.import_id)
remote_image = None remote_image = None
@ -470,8 +497,8 @@ class ImageConfig():
# update artifacts, if we've got 'em # update artifacts, if we've got 'em
if self.artifacts_yaml.exists(): if self.artifacts_yaml.exists():
yaml = YAML() self.artifacts = self.yaml.load(self.artifacts_yaml)
self.artifacts = yaml.load(self.artifacts_yaml)
else: else:
self.artifacts = None self.artifacts = None
@ -480,6 +507,50 @@ class ImageConfig():
self.state_updated = datetime.utcnow().isoformat() self.state_updated = datetime.utcnow().isoformat()
def _run(self, cmd, errmsg=None, errvals=[]):
log = self._log
p = Popen(cmd, stdout=PIPE, stdin=PIPE, encoding='utf8')
out, err = p.communicate()
if p.returncode:
if log:
if errmsg:
log.error(errmsg, *errvals)
log.error('COMMAND: %s', ' '.join(cmd))
log.error('EXIT: %d', p.returncode)
log.error('STDOUT:\n%s', out)
log.error('STDERR:\n%s', err)
raise RuntimeError
return out, err
def _save_checksum(self, file):
self._log.info("Calculating checksum for '%s'", file)
sha256_hash = hashlib.sha256()
with open(file, 'rb') as f:
for block in iter(lambda: f.read(4096), b''):
sha256_hash.update(block)
with open(str(file) + '.sha256', 'w') as f:
print(sha256_hash.hexdigest(), file=f)
# convert local QCOW2 to format appropriate for a cloud
def convert_image(self):
self._log.info('Converting %s to %s', self.local_path, self.image_path)
self._run(
self.CONVERT_CMD[self.image_format] + [self.local_path, self.image_path],
errmsg='Unable to convert %s to %s', errvals=[self.local_path, self.image_path],
)
self._save_checksum(self.image_path)
self.built = datetime.utcnow().isoformat()
def save_metadata(self, upload=True):
os.makedirs(self.local_dir, exist_ok=True)
self._log.info('Saving image metadata')
self._yaml.dump(dict(self.tags), self.image_metadata_path)
self._save_checksum(self.image_metadata_path)
class DictObj(dict): class DictObj(dict):

View File

@ -1,3 +1,4 @@
# vim: ts=2 et: # vim: ts=2 et:
builder = qemu
# TBD # TBD

View File

@ -26,9 +26,6 @@ case "$CLOUD" in
aws) aws)
DATASOURCE="Ec2" DATASOURCE="Ec2"
;; ;;
nocloud)
DATASOURCE="NoCloud"
;;
*) *)
echo "Unsupported Cloud '$CLOUD'" >&2 echo "Unsupported Cloud '$CLOUD'" >&2
exit 1 exit 1

View File

@ -4,12 +4,12 @@ set -x
echo "Are you really sure as AMIs might be used by customers !!" echo "Are you really sure as AMIs might be used by customers !!"
read read
#TAG_FILTER="Name=tag:project,Values=zdt-alpine" TAG_FILTER="Name=tag:Name,Values=*-uefi-*"
TAG_FILTER="Name=tag:Name,Values=zdt-alpine-3.16.2-x86_64-bios-tiny-minimal-r1" # TAG_FILTER="Name=tag:Name,Values=zdt-alpine-3.16.2-x86_64-bios-tiny-kubezero-1.23.10-r0"
#TAG_FILTER="Name=tag:Name,Values=zdt-alpine-3.16.2-x86_64-bios-tiny-kubezero-1.23.10-r0" # TAG_FILTER="Name=tag:Name,Values=zdt-alpine-3.16.2-x86_64-bios-tiny-minimal-r2"
#for r in $(aws ec2 describe-regions --query "Regions[].{Name:RegionName}" --output text); do #for r in $(aws ec2 describe-regions --query "Regions[].{Name:RegionName}" --output text); do
for r in ap-southeast-2 ca-central-1 eu-central-1 us-east-1 us-west-1 us-west-2; do for r in eu-central-1 us-west-2 ap-southeast-2 ca-central-1 us-east-1 us-west-1; do
amis=$(aws ec2 describe-images --region $r --owners self --output json --filters $TAG_FILTER | jq -r '.Images[].ImageId') amis=$(aws ec2 describe-images --region $r --owners self --output json --filters $TAG_FILTER | jq -r '.Images[].ImageId')
for a in $amis; do for a in $amis; do
aws ec2 deregister-image --region $r --image-id $a && echo "Deleted AMI $a in $r" aws ec2 deregister-image --region $r --image-id $a && echo "Deleted AMI $a in $r"

View File

@ -16,6 +16,7 @@ dhclient = true
monit = true monit = true
busybox-extras = true busybox-extras = true
tcpdump = true tcpdump = true
uuidgen = true
neofetch = edge-community neofetch = edge-community
tiny-cloud = edge-main tiny-cloud = edge-main
tiny-cloud-openrc = edge-main tiny-cloud-openrc = edge-main

View File

@ -16,4 +16,11 @@ WHEN {
py3-boto3 = true py3-boto3 = true
} }
} }
# Disable KVM during cross build
aarch64 {
qemu.args = [
[-machine, "type=virt"],
]
}
} }

View File

@ -5,7 +5,7 @@ include required("common.conf")
packages { include required("kubezero-packages.conf") } packages { include required("kubezero-packages.conf") }
description = [ "- https://kubezero.com" ] description = [ "- https://kubezero.com" ]
name = [ kubezero-1.23.11 ] name = [ kubezero-1.24.7 ]
# size = 2G # size = 2G
WHEN { WHEN {

View File

@ -5,7 +5,7 @@ project = zdt-alpine
# all build configs start with these # all build configs start with these
Default { Default {
project = ${project} project = ${project}
kubeversion = 1.23 kubeversion = 1.24
# image name/description components # image name/description components
name = [ zdt-alpine ] name = [ zdt-alpine ]
@ -35,10 +35,10 @@ Dimensions {
} }
arch { arch {
x86_64 { include required("arch/x86_64.conf") } x86_64 { include required("arch/x86_64.conf") }
# aarch64 { include required("arch/aarch64.conf") } aarch64 { include required("arch/aarch64.conf") }
} }
firmware { firmware {
bios { include required("firmware/bios.conf") } #bios { include required("firmware/bios.conf") }
uefi { include required("firmware/uefi.conf") } uefi { include required("firmware/uefi.conf") }
} }
bootstrap { bootstrap {
@ -68,6 +68,9 @@ Mandatory {
name = [ "r{revision}" ] name = [ "r{revision}" ]
encrypted = "alias/zdt/amis" encrypted = "alias/zdt/amis"
# We use neofetch custom branding
motd = {}
# final provisioning script # final provisioning script
scripts = [ cleanup ] scripts = [ cleanup ]

View File

@ -10,13 +10,13 @@ TARGET=/mnt
echo "@kubezero https://cdn.zero-downtime.net/alpine/v${VERSION}/kubezero" >> "$TARGET/etc/apk/repositories" echo "@kubezero https://cdn.zero-downtime.net/alpine/v${VERSION}/kubezero" >> "$TARGET/etc/apk/repositories"
wget -q -O $TARGET/etc/apk/keys/stefan@zero-downtime.net-61bb6bfb.rsa.pub https://cdn.zero-downtime.net/alpine/stefan@zero-downtime.net-61bb6bfb.rsa.pub wget -q -O $TARGET/etc/apk/keys/stefan@zero-downtime.net-61bb6bfb.rsa.pub https://cdn.zero-downtime.net/alpine/stefan@zero-downtime.net-61bb6bfb.rsa.pub
# Install custom sysctl settings # Install ZDT packages here after repo is available
cp $SETUP/zdt-sysctl.conf $TARGET/etc/sysctl.d/60-zdt.conf
# Install fluent-bit
apk -U --root "$TARGET" --no-cache add \ apk -U --root "$TARGET" --no-cache add \
fluent-bit@kubezero fluent-bit@kubezero
# Install custom sysctl settings
cp $SETUP/zdt-sysctl.conf $TARGET/etc/sysctl.d/60-zdt.conf
# Fix dhcp to set MTU properly # Fix dhcp to set MTU properly
install -o root -g root -Dm644 -t $TARGET/etc/dhcp $SETUP/dhclient.conf install -o root -g root -Dm644 -t $TARGET/etc/dhcp $SETUP/dhclient.conf
echo 'Setup dhclient' echo 'Setup dhclient'
@ -67,7 +67,6 @@ mv $TARGET/etc/profile.d/color_prompt.sh.disabled $TARGET/etc/profile.d/color_pr
echo 'alias rs="doas bash"' > $TARGET/etc/profile.d/alias.sh echo 'alias rs="doas bash"' > $TARGET/etc/profile.d/alias.sh
# branding # branding
rm -f $TARGET/etc/motd
cp $SETUP/neofetch.conf $TARGET/etc/neofetch.conf cp $SETUP/neofetch.conf $TARGET/etc/neofetch.conf
cp $SETUP/zdt-ascii.txt $TARGET/etc/neofetch-logo.txt cp $SETUP/zdt-ascii.txt $TARGET/etc/neofetch-logo.txt
echo '[ -n "$SSH_TTY" -a "$SHLVL" -eq 1 ] && neofetch --config /etc/neofetch.conf' > $TARGET/etc/profile.d/motd.sh echo '[ -n "$SSH_TTY" -a "$SHLVL" -eq 1 ] && neofetch --config /etc/neofetch.conf' > $TARGET/etc/profile.d/motd.sh

View File

@ -6,7 +6,7 @@
SETUP=/tmp/setup.d SETUP=/tmp/setup.d
TARGET=/mnt TARGET=/mnt
KUBE_VERSION=1.23 KUBE_VERSION=1.24
AWS_IAM_VERSION=0.5.9 AWS_IAM_VERSION=0.5.9
apk -U --root "$TARGET" --no-cache add \ apk -U --root "$TARGET" --no-cache add \
@ -15,9 +15,14 @@ apk -U --root "$TARGET" --no-cache add \
kubelet@kubezero=~$KUBE_VERSION \ kubelet@kubezero=~$KUBE_VERSION \
kubectl@kubezero=~$KUBE_VERSION \ kubectl@kubezero=~$KUBE_VERSION \
ecr-credential-provider@kubezero=~$KUBE_VERSION \ ecr-credential-provider@kubezero=~$KUBE_VERSION \
aws-iam-authenticator@kubezero=~$AWS_IAM_VERSION \ aws-iam-authenticator@kubezero=~$AWS_IAM_VERSION
aws-neuron-driver@kubezero \
nvidia-open-gpu@kubezero # Only install custom kernel modules for X86_64
if [ "$ARCH" == "x86_64" ]; then
apk -U --root "$TARGET" --no-cache add \
aws-neuron-driver@kubezero \
nvidia-open-gpu@kubezero
fi
# Pre-load container images # Pre-load container images
# echo 'Pre-loaded Kubernetes control container images' # echo 'Pre-loaded Kubernetes control container images'

View File

@ -1,5 +1,5 @@
print_info() { print_info() {
echo -e "\n\n" echo -e "\n"
prin "$(color 1)Welcome to Alpine - ZeroDownTime edition" prin "$(color 1)Welcome to Alpine - ZeroDownTime edition"
echo echo
@ -11,6 +11,7 @@ print_info() {
info title info title
info underline info underline
info "OS" distro
info "Host" model info "Host" model
info "Kernel" kernel info "Kernel" kernel
info "Uptime" uptime info "Uptime" uptime
@ -27,7 +28,7 @@ print_info() {
info underline info underline
} }
title_fqdn="on" title_fqdn="off"
memory_percent="on" memory_percent="on"
colors=(1 2 15 15 15 15) colors=(1 2 15 15 15 15)
image_source="/etc/neofetch-logo.txt" image_source="/etc/neofetch-logo.txt"

View File

@ -1,10 +1,10 @@
qemu = { qemu = {
"boot_wait": { "boot_wait": {
"aarch64": "15s", "aarch64": "2m",
"x86_64": "15s" "x86_64": "30s"
} }
cmd_wait = "5s" cmd_wait = "5s"
ssh_timeout = "20s" ssh_timeout = "1m"
memory = 1024 memory = 1024
} }