Skip to content

Commit

Permalink
Add bootc image builder case
Browse files Browse the repository at this point in the history
utilities and case scripts are provided,
and cover Image type:ami vmdk qcow2 anaconda-iso raw

container image reference: quay.io/centos-bootc/centos-bootc:stream9,
quay.io/centos-bootc/fedora-bootc:eln, localhost/bootc:eln

Signed-off-by: Chunfu Wen <[email protected]>
  • Loading branch information
chunfuwen committed Mar 29, 2024
1 parent 3a28269 commit 747307d
Show file tree
Hide file tree
Showing 5 changed files with 663 additions and 0 deletions.
346 changes: 346 additions & 0 deletions provider/bootc_image_builder/bootc_image_build_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,346 @@
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# Copyright Red Hat
#
# SPDX-License-Identifier: GPL-2.0
#
# Author: [email protected]
#
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""Helper functions for bootc image builder"""

import logging
import json
import os
import shutil
import time
import textwrap
import pathlib

from avocado.utils import path, process
from avocado.core import exceptions
from virttest import utils_package

LOG = logging.getLogger('avocado.' + __name__)


def install_bib_packages():
"""
install necessary bootc image builder necessary packages
"""
package_list = ["podman", "skopeo", "virt-install", "curl", "virt-manager"]
for pkg in package_list:
try:
path.find_command(pkg)
except path.CmdNotFoundError:
utils_package.package_install(pkg)


def podman_command_build(bib_image_url, disk_image_type, image_ref, config=None, local_container=False, tls_verify="true", chownership=None, options=None, **dargs):
"""
Use podman run command to launch bootc image builder
:param bib_image_url: bootc image builder url
:param disk_image_type: image type to build [qcow2, ami] (default "qcow2")
:param image_ref: image reference
:param config: config file
:param local_container: whether use local container image
:param tls_verify: whether verify tls connection
:param local_container: whether use local container image
:param chownership: whether change output ownership
:param options: additional options if needed
:param dargs: standardized virsh function API keywords
:return: CmdResult object
"""
if not os.path.exists("/var/lib/libvirt/images/output"):
os.makedirs("/var/lib/libvirt/images/output")
cmd = "sudo podman run --rm -it --privileged --pull=newer --security-opt label=type:unconfined_t -v /var/lib/libvirt/images/output:/output"
if config:
cmd += " -v %s:/config.json " % config

if local_container:
cmd += " -v /var/lib/containers/storage:/var/lib/containers/storage "

cmd += " %s " \
" --type %s --tls-verify=%s " % (bib_image_url, disk_image_type, tls_verify)

if config:
cmd += " --config /config.json "

if local_container:
cmd += " --local %s " % image_ref
else:
cmd += " %s " % image_ref

if chownership:
cmd += " --chown %s " % chownership

if options is not None:
cmd += " %s" % options

debug = dargs.get("debug", True)

ignore_status = dargs.get("ignore_status", False)
timeout = int(dargs.get("timeout", "1800"))
LOG.debug("the whole podman command: %s\n" % cmd)

ret = process.run(
cmd, timeout=timeout, verbose=debug, ignore_status=ignore_status, shell=True)

ret.stdout = ret.stdout_text
ret.stderr = ret.stderr_text
return ret


def podman_login(podman_username, podman_password, registry):
"""
Use podman to login in registry
:param podman_username: podman username
:param podman_password: podman password
:param registry: registry to login
:return: CmdResult object
"""
command = "sudo podman login -u='%s' -p='%s' %s " % (podman_username, podman_password, registry)
process.run(
command, timeout=60, verbose=True, ignore_status=False, shell=True)


def podman_push(podman_username, podman_password, registry, container_url):
"""
Use podman image to registry
:param podman_username: podman username
:param podman_password: podman password
:param registry: registry to login
:param container_url: image url
:return: CmdResult object
"""
podman_login(podman_username, podman_password, registry)
command = "sudo podman push %s " % container_url
process.run(
command, timeout=1200, verbose=True, ignore_status=False, shell=True)


def create_config_json_file(folder, username, password):
"""
install necessary bootc image builder necessary packages
:param folder: the folder that config.json reside in
:param username: user name
:param password: user password
"""
public_key_path = os.path.join(os.path.expanduser("~/.ssh/"), "id_rsa.pub")
if not os.path.exists(public_key_path):
LOG.debug("public key doesn't exist, please create one")
key_gen_cmd = "ssh-keygen -q -t rsa -N '' <<< $'\ny' >/dev/null 2>&1"
process.run(key_gen_cmd, shell=True, ignore_status=False)

with open(public_key_path, 'r') as ssh:
key_value = ssh.read().rstrip()
cfg = {
"blueprint": {
"customizations": {
"user": [
{
"name": username,
"password": password,
"groups": ["wheel"],
"key": "%s" % key_value,
},
],
},
},
}
LOG.debug("what is cfg:%s", cfg)
config_json_path = pathlib.Path(folder) / "config.json"
config_json_path.write_text(json.dumps(cfg), encoding="utf-8")
return os.path.join(folder, "config.json")


def create_and_build_container_file(folder, build_container, container_tag, add_vmware_tool=False):
"""
Create container file and build container tag
:param folder: the folder that config.json reside in
:param build_container: the base container image
:param container_tag: container tag
:param add_vmware_tool: whether add vmware tool into container
"""
# clean up existed image
clean_image_cmd = "sudo podman rmi %s" % container_tag
process.run(clean_image_cmd, shell=True, ignore_status=True)
etc_config = ''
dnf_vmware_tool = ''

# create VmWare tool folder
if add_vmware_tool:
vmware_tool_path = os.path.join(folder, "etc/vmware-tools/")
if not os.path.exists(vmware_tool_path):
os.makedirs(vmware_tool_path)
etc_config = "COPY etc/ /etc/"
dnf_vmware_tool = "dnf -y install open-vm-tools && dnf clean all && systemctl enable vmtoolsd.service && "

download_vmware_config_cmd = "curl https://gitlab.com/bootc-org/" \
"examples/-/raw/main/vmware/etc/vmware-tools/tools.conf > %s/tools.conf" % vmware_tool_path
process.run(download_vmware_config_cmd, shell=True, verbose=True, ignore_status=True)

container_path = pathlib.Path(folder) / "Containerfile_tmp"
shutil.copy("/etc/yum.repos.d/beaker-BaseOS.repo", folder)
shutil.copy("/etc/yum.repos.d/beaker-AppStream.repo", folder)
container_path.write_text(textwrap.dedent(f"""\n
FROM {build_container}
{etc_config}
COPY beaker-BaseOS.repo /etc/yum.repos.d/
COPY beaker-AppStream.repo /etc/yum.repos.d/
RUN {dnf_vmware_tool} dnf install -y vim && dnf clean all
"""), encoding="utf8")
build_cmd = "sudo podman build -t %s -f %s" % (container_tag, str(container_path))
process.run(build_cmd, shell=True, ignore_status=False)


def create_and_start_vmware_vm(params):
"""
prepare environment, upload vmdk, create and start vm
@param params: one dictionary wrapping various parameter
"""
install_vmware_govc_tool()
setup_vCenter_env(params)
delete_vm_if_existed(params)
import_vmdk_to_vCenter(params)
create_vm_in_vCenter(params)
attach_disk_to_vm(params)
power_on_vm(params)


def install_vmware_govc_tool():
"""
Download VmWare govc tool and install it
"""
govc_install_cmd = "curl -L -o - 'https://github.com/vmware/govmomi/releases/latest/download/govc_Linux_x86_64.tar.gz' " \
"| tar -C /usr/local/bin -xvzf - govc"
if not os.path.exists("/usr/local/bin/govc"):
process.run(govc_install_cmd, shell=True, ignore_status=False)


def setup_vCenter_env(params):
"""
Download VmWare govc tool and install it
@param params: one dictionary wrapping various parameter
"""
# vCenter information
os.environ["GOVC_URL"] = params.get("GOVC_URL")
os.environ["GOVC_USERNAME"] = params.get("GOVC_USERNAME")
os.environ["GOVC_PASSWORD"] = params.get("GOVC_PASSWORD")
os.environ["DATA_CENTER"] = params.get("DATA_CENTER")
os.environ["DATA_STORE"] = params.get("DATA_STORE")
os.environ["GOVC_INSECURE"] = "true"
process.run("govc about", shell=True, ignore_status=False)


def import_vmdk_to_vCenter(params):
"""
import vmdk into vCenter
@param params: one dictionary wrapping various parameter
"""
import_cmd = f"govc import.vmdk -force {params.get('vm_disk_image_path')}"
process.run(import_cmd, shell=True, verbose=True, ignore_status=False)


def create_vm_in_vCenter(params):
"""
create VM in vCenter
@param params: one dictionary wrapping various parameter
"""
create_cmd = "govc vm.create -net='VM Network' -on=false -c=2 " \
"-m=4096 -g=centos9_64Guest -firmware=%s %s" % (params.get("firmware"), params.get("vm_name_bootc"))
process.run(create_cmd, shell=True, verbose=True, ignore_status=False)


def attach_disk_to_vm(params):
"""
attach disk to VM in vCenter
@param params: one dictionary wrapping various parameter
"""
vm_name = params.get("vm_name_bootc")
attach_cmd = "govc vm.disk.attach -vm %s" \
" -controller %s -link=false -disk=%s/%s.vmdk" % (vm_name, params.get("controller"), vm_name, vm_name)
process.run(attach_cmd, shell=True, verbose=True, ignore_status=False)


def power_on_vm(params):
"""
power on VM in vCenter
@param params: one dictionary wrapping various parameter
"""
vm_name = params.get("vm_name_bootc")
power_on_cmd = "govc vm.power -on=true %s" % vm_name
process.run(power_on_cmd, shell=True, verbose=True, ignore_status=False)
time.sleep(40)
state_cmd = "govc vm.info -json %s |jq -r .virtualMachines[0].summary.runtime.powerState" % vm_name
result = process.run(state_cmd, shell=True, verbose=True, ignore_status=False).stdout_text.strip()
if result not in "poweredOn":
raise exceptions.TestFail('The VM state is not powered on,actually is: %s') % result


def delete_vm_if_existed(params):
"""
delete vm if existed
@param params: one dictionary wrapping various parameter
"""
vm_name = params.get("vm_name_bootc")
find_cmd = "govc find / -type m -name %s" % vm_name
cmd_result = process.run(find_cmd, shell=True, verbose=True, ignore_status=True).stdout_text
if cmd_result:
vm_path = cmd_result.strip()
delete_cmd = "govc vm.destroy %s && govc datastore.rm -f %s" % (vm_name, vm_name)
process.run(delete_cmd, shell=True, verbose=True, ignore_status=True)


def parse_container_url(params):
"""
Parse repository information from container url
@param params: wrapped dictionary containing url
"""
container_url = params.get("container_url")
repository_info = container_url.split('/')[-1]
repository_name = repository_info.split(':')[0]
if "localhost" in container_url:
repository_name = "localhost-%s" % repository_name
return repository_name


def convert_disk_image_name(params):
"""
Convert disk type image name
@param params: wrapped dictionary containing parameters
"""
repository_name = parse_container_url(params)
origin_disk_name, extension = os.path.splitext(params.get("output_name"))
dest_disk_name = "%s-%s%s" % (origin_disk_name, repository_name, extension)
return dest_disk_name


def get_group_and_user_ids(folder_path):
try:
stat_info = os.stat(folder_path)
gid = stat_info.st_gid
uid = stat_info.st_uid
return gid, uid
except FileNotFoundError:
LOG.debug(f"Folder '{folder_path}' not found.")
return None, None
except Exception as ex:
LOG.debug(f"Error occurred: {ex}")
return None, None
Loading

0 comments on commit 747307d

Please sign in to comment.