Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 128 additions & 73 deletions contrib/create-image.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import shutil
import subprocess
import sys
from enum import Enum
from functools import cache

from print_color import print
from prettytable import PrettyTable
from typing import Any
Expand All @@ -14,6 +17,25 @@
import argparse


class ArchTypes(str, Enum):
AMD64 = 'amd64'
ARM64 = 'arm64'
BOTH = "all"

def __str__(self):
return self.value


def arch_type_strategy(arg_value: str):
try:
return ArchTypes[arg_value.upper()]
except KeyError:
raise argparse.ArgumentTypeError(
f"Invalid option: '{arg_value.upper}'. Valid options are: "
f"{', '.join(c.name.lower() for c in ArchTypes)}")


@cache
def get_current_git_branch_name() -> str:
result = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
Expand All @@ -33,21 +55,21 @@ def ignore_tags_constructor(loader, node):
yaml.SafeLoader.add_constructor(None, ignore_tags_constructor)
variant_data = dict()

with open(f"{run_dir}/.zuul.yaml", "r") as file:
with open(f"{RUN_DIR}/.zuul.yaml", "r") as file:
zuul_config = yaml.safe_load(file)
for element in zuul_config:
for elem_name, elem_data in element.items():
if elem_name != "job" or not re.fullmatch(
r"node-image-build-.+", elem_data["name"]
r"node-image-build-.+", elem_data["name"]
):
continue
variant_data[elem_data["name"]] = elem_data["vars"]
return variant_data


def build_template(context: dict[str, Any]) -> str:
results_filename = f"{build_dir}/user-data-{context['variant']}.yaml"
template_loader = FileSystemLoader(searchpath=f"{run_dir}/templates/")
def build_template(context: dict[str, Any], result_build_dir) -> str:
results_filename = f"{result_build_dir}/user-data-{context['variant']}.yaml"
template_loader = FileSystemLoader(searchpath=f"{RUN_DIR}/templates/")
template_env = Environment(loader=template_loader, undefined=StrictUndefined)
results_template = template_env.get_template("user-data.yml.j2")
with open(results_filename, mode="w", encoding="utf-8") as results:
Expand All @@ -59,36 +81,64 @@ def build_template(context: dict[str, Any]) -> str:
return results_filename


def docker_run(cmd: str, working_dir: str, chown_glob="*.iso"):
@cache
def get_create_build_container(release: str):
print()
os.chdir(run_dir)
os.chdir(RUN_DIR)

branch = get_current_git_branch_name()
docker_build_image = f"osism-node-image-builder:latest-{branch}"

print("*" * 50, color="green")
print(f"** DOCKER BUILD: {docker_build_image}", color="green")

# TODO: reactivate
# f"docker build --network=host -t {DOCKER_BUILD_IMAGE} "
cmd = f"docker build --load -t {docker_build_image} --build-arg UBUNTU_VERSION={release} -f Dockerfile ."

subprocess.run(
f"docker build --network=host -t {DOCKER_BUILD_IMAGE} "
+ f"--build-arg UBUNTU_VERSION={DISTRIBUTION} -f Dockerfile .",
cmd.split(),
check=True,
shell=True,
)
print("DOCKER:", color="green")
print(f"+ {cmd}")
print("**", color="green")
print("*" * 50, color="green")
return docker_build_image

chown_command = ""
if chown_glob and chown_glob != "":
chown_command = f"&& chown -vR {os.getuid()}:{os.getgid()} {chown_glob}"

def docker_run(cmd: str, working_dir: str, cargs: argparse.Namespace):
os.chdir(RUN_DIR)
get_create_build_container(cargs.release)
print()
os.chdir(RUN_DIR)
print("*" * 50, color="green")
print("** DOCKER:", color="green")

if ";" in cmd:
cmd = f'bash -c "{cmd}"'
print(f"+ {cmd}")

cmd_docker = (
f"docker run --rm --net=host -v {working_dir}:/work "
+ f"{DOCKER_BUILD_IMAGE} "
+ f'bash -c "{cmd} {chown_command}"'
"docker run -ti --rm --net=host "
f"-v {RUN_DIR}:/work "
f"-v {working_dir}/:/work/build "
f"--workdir /work/build/ "
f"{get_create_build_container(cargs.release)} {cmd}"
)

try:
subprocess.run(cmd_docker, check=True, shell=True)
res = subprocess.run(cmd_docker, check=True, shell=True)
if res.returncode != 0:
print(f"FAILED: Command >>>{cmd_docker}<<< returned non-zero exit status {res.returncode}.", color="red")
sys.exit(res.returncode)
except subprocess.CalledProcessError as e:
print(f"Command {e.cmd} returned non-zero exit status {e.returncode}.")
print(f"FAILED: Command >>>{cmd_docker}<<< failed :\n\n{e}", color="red")
sys.exit(e.returncode)
print("**", color="green")
print("*" * 50, color="green")


def package_ffr_files(context: dict) -> str:
frr_dir = f"{build_dir}/media/frr"
def package_ffr_files(context: dict, arch_dir: str, cargs: argparse.Namespace) -> str:
frr_dir = f"{arch_dir}/media/frr"
os.makedirs(frr_dir, exist_ok=True)

packages = [
Expand All @@ -113,16 +163,14 @@ def package_ffr_files(context: dict) -> str:
download_command = f"apt-get update && apt-get download {' '.join(packages)}"
download_ready = f"{frr_dir}/.download_ready"
if not os.path.exists(download_ready):
docker_run(download_command, frr_dir, ".")
docker_run(download_command, frr_dir, cargs)
with open(f"{frr_dir}/.download_ready", "w"):
pass # do nothing, just create an empty file

env = Environment(loader=FileSystemLoader("/"))
for file_name in glob.glob(f"{run_dir}/templates/frr/*"):
for file_name in glob.glob(f"{RUN_DIR}/templates/frr/*"):
if file_name.endswith(".j2"):
target_filename = (
frr_dir + "/" + os.path.basename(file_name.removesuffix(".j2"))
)
target_filename = ( frr_dir + "/" + os.path.basename(file_name.removesuffix(".j2")) )
print(f"rendering file : {target_filename}", color="magenta")
template = env.get_template(file_name)
with open(target_filename, mode="w", encoding="utf-8") as results:
Expand All @@ -136,46 +184,44 @@ def package_ffr_files(context: dict) -> str:


def build_image(
name: str, context_data: dict, template_only: bool = False
name: str, context_data: dict, cargs: argparse.Namespace, image_build_dir: str
) -> str | None:
print(f"build image >>>{name}<<<", color="magenta")
user_data_file = build_template(context_data)
print()
print(f"build image >>>{name}<<< for architecture {cargs.arch}", color="magenta")
print()
user_data_file = build_template(context_data, image_build_dir)

add_dir = ""
if context_data.get("layer3_underlay") == "true":
add_dir = package_ffr_files(context_data)
add_dir = package_ffr_files(context_data, image_build_dir, cargs)

iso_file = None

if not template_only:
iso_file = f"ubuntu-autoinstall-{context_data['variant']}.iso"
if not cargs.template_only:
iso_file = f"ubuntu-autoinstall-{context_data['variant']}_{args.arch}.iso"
iso_file_checksum = f"{iso_file}.CHECKSUM"

print(f"image {iso_file}", color="magenta")
os.chdir(run_dir)
print()
print(f"image {iso_file}", color="yellow")
print()
os.chdir(RUN_DIR)

build_command = (
"/work/contrib/image-create.sh -r -a -k "
+ f"-u /work/build/{os.path.basename(user_data_file)} -n {DISTRIBUTION} "
+ f"--destination {iso_file} {add_dir}"
f"/work/contrib/image-create.sh -r -a -k "
f"--cpu-arch {cargs.arch} "
"--timeout 3 --banner 'Install OSISM Node' "
f"-u /work/build/{os.path.basename(user_data_file)} -n {cargs.release} "
f"--destination /work/build/{iso_file} {add_dir}"
)
docker_run(build_command, run_dir)

iso_file_checksum = f"{iso_file}.CHECKSUM"
print(f"Creating checksum file {iso_file_checksum}", color="magenta")
try:
subprocess.run(
f"shasum -a 256 {iso_file} > {iso_file_checksum}",
check=True,
shell=True,
)
except subprocess.CalledProcessError as e:
print(f"Command {e.cmd} returned non-zero exit status {e.returncode}.")
docker_run(build_command, image_build_dir, cargs)
docker_run(f"sha256sum {iso_file} > {iso_file_checksum}", image_build_dir, cargs)
docker_run(f"chown -vR {os.getuid()}:{os.getgid()} *.iso *.iso.CHECKSUM", image_build_dir, cargs)

return iso_file


def create_context(
image_name: str, commandline_args: argparse.Namespace
image_name: str, commandline_args: argparse.Namespace
) -> dict[str, str]:
context_data_default = {
"layer3_underlay": "false",
Expand Down Expand Up @@ -217,7 +263,7 @@ def create_context(
return context_data


def build_images(commandline_args: argparse.Namespace):
def build_images(commandline_args: argparse.Namespace, image_build_dir: str):
os.makedirs(build_dir, exist_ok=True)
images = set()
for image in commandline_args.build:
Expand All @@ -233,15 +279,16 @@ def build_images(commandline_args: argparse.Namespace):
build_image(
image,
create_context(image, commandline_args),
commandline_args.template_only,
commandline_args,
image_build_dir,
)


def wrap_text_by_words(text: str, words_per_line: int):
words = text.split(" ")
lines = []
for i in range(0, len(words), words_per_line):
line = " ".join(words[i : i + words_per_line]).strip()
line = " ".join(words[i: i + words_per_line]).strip()
lines.append(line)

return "\n".join(lines)
Expand All @@ -265,20 +312,17 @@ def show_variants():
print(table)
print()
print(
f"A overview: https://github.com/osism/node-image/tree/{BRANCH}", color="green"
f"A overview: https://github.com/osism/node-image/tree/{get_current_git_branch_name()}", color="green"
)
sys.exit(0)


DISTRIBUTION = "jammy"
DOCKER_WORKDIR = "/work"
BRANCH = get_current_git_branch_name()
DOCKER_BUILD_IMAGE = f"osism-node-image-builder:latest-{BRANCH}"
RUN_DIR = os.path.realpath(os.path.dirname(os.path.realpath(__file__)) + "/../")

if __name__ == "__main__":
run_dir = os.path.realpath(os.path.dirname(os.path.realpath(__file__)) + "/../")

parser = argparse.ArgumentParser(prog=f"{run_dir}/create-image.sh")
parser = argparse.ArgumentParser(prog=f"{RUN_DIR}/create-image.sh")

exclusive_group = parser.add_mutually_exclusive_group(required=True)
exclusive_group.add_argument(
Expand All @@ -291,7 +335,7 @@ def show_variants():
"--env", "-e", action="store_true", help="Create build environment"
)
exclusive_group.add_argument(
"--clean", "-r", action="store_true", help="Drop cached build data"
"--drop", "-d", action="store_true", help="Drop cached build data"
)

parser.add_argument(
Expand All @@ -303,6 +347,18 @@ def show_variants():
default=[],
)
parser.add_argument("--config", "-c", type=str, help="A config as yaml file")

parser.add_argument("--arch", "-a",
type=arch_type_strategy,
choices=list(ArchTypes),
default=ArchTypes.AMD64,
help="Architecture")

parser.add_argument("--release", "-r",
type=str,
default="jammy",
help="Architecture")

parser.add_argument(
"--build-directory", type=str, help="Overwrite the default build directory"
)
Expand All @@ -312,35 +368,34 @@ def show_variants():
)
args = parser.parse_args()

build_dir = f"{run_dir}/build"
build_dir = f"{RUN_DIR}/build/{args.arch}/{args.release}"

os.chdir(run_dir)
os.chdir(RUN_DIR)
os.umask(0o022)

variants = get_variants_data()

if args.show:
show_variants()

if args.clean:
if args.drop:
print("Cleaning up cached build data", color="green")
if os.path.exists(build_dir):
shutil.rmtree(build_dir)
os.makedirs(build_dir)
if (
len(
subprocess.check_output(
f"docker images {DOCKER_BUILD_IMAGE} -q ", shell=True
len(
subprocess.check_output(
f"docker images {get_create_build_container(args.release)} -q ", shell=True
)
)
)
> 0
> 0
):
subprocess.run(f"docker rmi {DOCKER_BUILD_IMAGE}", check=True, shell=True)
subprocess.run(f"docker rmi {get_create_build_container(args.release)}", check=True, shell=True)
sys.exit(0)

if args.build:
build_images(args)
build_images(args, build_dir)
sys.exit(0)

if args.env:
docker_run("cat /etc/lsb-release", run_dir, chown_glob="")
docker_run("cat /etc/lsb-release", RUN_DIR, args)
Loading