tools/selfhost/bootstrap.py (455 lines of code) (raw):
#!/usr/bin/env python3
import argparse
import json
import jsonschema
import os
import requests
import shutil
import subprocess
import sys
from jsonschema import validate
from urllib.parse import urlparse
Verbose = False
# where some useful self-hoster bugbash files will be copied
content_dir = '/tmp/deltaupdates'
def get_filepath_relative_to_script_dir(filename: str) -> str:
"""Builds a file path relative to the location where this script"""
script_dir = os.path.dirname(sys.argv[0])
return os.path.join(script_dir, filename)
def mkdirs_if_not_exist(dir_path: str) -> None:
"""makes the dir and its intermediates if it doesn't already exist."""
if not os.path.exists(dir_path):
os.makedirs(dir_path, mode=0o777, exist_ok=True)
def get_schema() -> str:
"""This function loads the boostrap config schema."""
bootstrap_config_schema_path = get_filepath_relative_to_script_dir(
filename='bootstrap_config_schema.json'
)
with open(bootstrap_config_schema_path, 'r') as file:
schema = json.load(file)
return schema
def validate_config(json_data):
"""Validates the json_data against the config json schema."""
execute_api_schema = get_schema()
try:
validate(instance=json_data, schema=execute_api_schema)
except jsonschema.exceptions.ValidationError as err:
print(err)
err = "Config JSON data is Invalid"
return False, err
err = None
return True, err
def get_config_example() -> str:
"""Returns the contents of the config example file"""
example_json_path = get_filepath_relative_to_script_dir(
filename='bootstrap_config_example.json'
)
with open(example_json_path, 'r') as file:
contents = json.load(file)
return contents
def run_cmd(cmd: str) -> int:
# ubuntu18.04 has python 3.6 so can't use capture_output=VERBOSE so set
# stdout and stderr to PIPE instad
ret = subprocess.run(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, shell=True
)
ret_code = ret.returncode
if ret_code != 0:
print(f"Error: returncode {ret_code} for cmd '{cmd}'.{os.linesep}"
f"Contents of <stderr>:{os.linesep}{ret.stderr.decode()}"
)
elif Verbose:
print(ret.stdout.decode())
return ret_code
def install_do(script_dir):
"""Installs delivery optimization components from the bundle"""
print("Installing DeliveryOptimization packages ...")
tmp_dir = os.path.join(script_dir, 'do_package')
filename = 'ubuntu1804_x64-packages.tar'
script_tar_file = os.path.join(script_dir, filename)
ret_code = run_cmd(
f"mkdir -p {tmp_dir} "
f"&& cp {script_tar_file} {tmp_dir} "
f"&& cd {tmp_dir} "
f"&& tar -xf ./{filename}"
)
if ret_code != 0:
return ret_code
# note: These are 0.8.3 .deb packages but in the tarball they still have
# 0.6.0 in the name.
do_agent_pkg = os.path.join(
tmp_dir,
'deliveryoptimization-agent_0.6.0_amd64.deb'
)
ret_code = run_cmd(f"apt-get -y install {do_agent_pkg}")
if ret_code != 0:
return ret_code
libdo_pkg = os.path.join(
tmp_dir,
'libdeliveryoptimization_0.6.0_amd64.deb'
)
ret_code = run_cmd(f"apt-get -y install {libdo_pkg}")
if ret_code != 0:
return ret_code
libdo_apt_plugin_pkg = os.path.join(
tmp_dir,
'deliveryoptimization-plugin-apt_0.4.0_amd64.deb'
)
ret_code = run_cmd(f"apt-get -y install {libdo_apt_plugin_pkg}")
if ret_code != 0:
return ret_code
return 0
def install_du_agent(script_dir):
"""Installs device update agent component from the bundle"""
print("Installing DU Agent ...")
do_agent_pkg = os.path.join(script_dir, 'du_agent_pkg.deb')
ret_code = run_cmd(f"apt-get -y install {do_agent_pkg}")
if ret_code != 0:
return ret_code
return 0
def install_deltaupdate_swupdate_files(script_dir):
"""Installs swupdate-related binaries and configs from the bundle"""
print("Installing DeltaUpdate feature swupdate-related files ...")
tmp_dir = os.path.join(script_dir, 'delta_swupdate_files')
filename = 'delta_swupdate_files.tar.gz'
tarball_path = os.path.join(script_dir, filename)
print(f"Unpacking {filename} to {tmp_dir} ...")
ret_code = run_cmd(
f"mkdir -p {tmp_dir} "
f"&& cp {tarball_path} {tmp_dir} "
f"&& cd {tmp_dir} "
f"&& tar -xzvf {filename} "
)
if ret_code != 0:
return ret_code
print("Installing swupdate executuble and configs ...")
ret_code = run_cmd(
f"cd {tmp_dir} && sh ./install.sh "
)
if ret_code != 0:
return ret_code
print("Installing swupdate dependencies ...")
print(" - Installing zstd APT package ...")
ret_code = run_cmd(
f"cd {tmp_dir} && apt-get -y install zstd "
)
if ret_code != 0:
return ret_code
print(" - Installing build-essential APT package "
"(this will take a couple minutes) ...")
ret_code = run_cmd(
f"cd {tmp_dir} && apt-get -y install build-essential "
)
if ret_code != 0:
return ret_code
print(" - Installing libconfig-dev APT package ...")
ret_code = run_cmd(
f"cd {tmp_dir} && apt-get -y install libconfig-dev "
)
if ret_code != 0:
return ret_code
print("Setup swupdate .swu sw-versions and hwrevision ... ")
ret_code = run_cmd(
f"echo 'myboard 0.1' > /etc/hwrevision"
)
if ret_code != 0:
return ret_code
ret_code = run_cmd(
f"echo 0.1 > /etc/sw-versions"
)
if ret_code != 0:
return ret_code
print("Running swupdate check on bugbash .swu ...")
ret_code = run_cmd(
f"cd {tmp_dir} && sh ./check_swu.sh "
)
if ret_code != 0:
return ret_code
print("Doing swupdate dry-run of bugbash .swu ...")
ret_code = run_cmd(
f"cd {tmp_dir} && sh ./apply_dryrun_swu.sh "
)
if ret_code != 0:
return ret_code
global content_dir
print(f"copying swupdate-related files to {content_dir} ...")
ret_code = run_cmd(
f"mkdir -p {content_dir} "
f"&& cd {tmp_dir} && cp ./*.pem ./bugbash_amd64.swu "
f"./swupdatev2_handler_script.sh "
f"./check_swu.sh ./apply_dryrun_swu.sh {content_dir} "
)
if ret_code != 0:
return ret_code
print("Installing zstd package for delta update target .swu ...")
# install zstd APT package to allow swupdate to decompress
# *.ext4.zst in deltaupdate recompressed target .swu
ret_code = run_cmd(f"apt-get -y install zstd")
if ret_code != 0:
return ret_code
return 0
def install_deltaupdate_files(script_dir):
"""Installs deltaupdate tools from the bundle"""
print("Installing DeltaUpdate DeltaProcessor and DiffGen tools ...")
# Install .net core 5 runtime for DiffGen Tools
print("DiffGen - Installing .NET core 5 runtime ...")
print("DiffGen .netcore5 - download packages-microsoft-prod.deb")
ret_code = run_cmd("wget https://packages.microsoft.com/config/ubuntu/"
"18.04/packages-microsoft-prod.deb "
"-O packages-microsoft-prod.deb"
)
if ret_code != 0:
return ret_code
print("DiffGen .netcore5 - dpkg install packages-microsoft-prod.deb")
ret_code = run_cmd("dpkg -i packages-microsoft-prod.deb")
if ret_code != 0:
return ret_code
print("DiffGen .netcore5 - apt update")
ret_code = run_cmd("apt-get update")
if ret_code != 0:
return ret_code
print("DiffGen .netcore5 - apt install apt-transport-https")
ret_code = run_cmd("apt-get install -y apt-transport-https")
if ret_code != 0:
return ret_code
print("DiffGen .netcore5 - apt update")
ret_code = run_cmd("apt-get update")
if ret_code != 0:
return ret_code
print("DiffGen .netcore5 - apt install aspnetcore-runtime-5.0")
ret_code = run_cmd("apt-get install -y aspnetcore-runtime-5.0")
if ret_code != 0:
return ret_code
# unzip
filename = 'deltaupdate_files.zip'
zip_file = os.path.join(script_dir, filename)
global content_dir
print(f"DiffGen - Ensure unzip APT pkg is installed ...")
ret_code = run_cmd(f"apt-get -y install unzip")
if ret_code != 0:
return ret_code
if not os.path.exists(content_dir):
print(f"creating {content_dir} ...")
ret_code = run_cmd(f"mkdir -p {content_dir}")
if ret_code != 0:
return ret_code
print(f"DiffGen - copy {zip_file} to {content_dir} ...")
ret_code = run_cmd(f"cp {zip_file} {content_dir}")
if ret_code != 0:
return ret_code
print(f"DiffGen - unzip {content_dir}/{filename} ...")
ret_code = run_cmd(f"cd {content_dir} && unzip ./{filename}")
if ret_code != 0:
return ret_code
# install delta processor library
#
print("DeltaProcessor - Setup Ubuntu Toolchain PPA for newer glibc ...")
ret_code = run_cmd("sudo apt-get -y install software-properties-common")
if ret_code != 0:
return ret_code
ret_code = run_cmd(
"sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test"
)
if ret_code != 0:
return ret_code
print("DeltaProcessor - Installing gcc-9 g++-9 Dependencies "
"for loading libadudiffapi.so ..."
)
ret_code = run_cmd("apt-get -y install gcc-9 g++-9")
if ret_code != 0:
return ret_code
lib_filename = 'libadudiffapi.so'
delta_processor_lib_filepath = os.path.join(
content_dir,
'DeltaProcessor_x64',
lib_filename
)
print("installing Delta processor library "
f"{delta_processor_lib_filepath} ...")
ret_code = run_cmd(
f"cp {delta_processor_lib_filepath} /usr/local/lib/ "
f"&& ldconfig /usr/local/lib/{lib_filename}"
)
if ret_code != 0:
return ret_code
print("unzip DiffGenTool ...")
ret_code = run_cmd(
"cd /tmp/deltaupdates/FIT_DiffGenTool "
"&& unzip diffgen-tool.Release.x64-linux.zip"
)
if ret_code != 0:
return ret_code
print("Make DiffGen binaries executable ...")
ret_code = run_cmd(
"cd /tmp/deltaupdates/FIT_DiffGenTool/"
"diffgen-tool.Release.x64-linux "
"&& chmod 755 applydiff bsdiff bspatch compress_files.py DiffGenTool "
"dumpdiff dumpextfs recompress_and_sign_tool.py "
"recompress_tool.py sign_tool.py zstd_compress_file"
)
if ret_code != 0:
return ret_code
return 0
def bootstrap_system() -> int:
"""Bootstrap using expanded bundle in the dir of this script."""
print(f"{os.linesep}Attempting to bootstrap the system ...")
script_dir = os.path.dirname(sys.argv[0])
ret_val = run_cmd("apt-get update")
if ret_val != 0:
return ret_val
ret_val = install_do(script_dir)
if ret_val != 0:
return ret_val
ret_val = install_du_agent(script_dir)
if ret_val != 0:
return ret_val
ret_val = install_deltaupdate_swupdate_files(script_dir)
if ret_val != 0:
return ret_val
ret_val = install_deltaupdate_files(script_dir)
if ret_val != 0:
return ret_val
# update user and group onwership of /etc/adu/du-config.json if it exists
# now that adu user and group have been setup so that healthcheck passes.
if os.path.exists('/etc/adu/du-config.json'):
ret_val = run_cmd('chown adu:adu /etc/adu/du-config.json')
if ret_val != 0:
return ret_val
print("Very last step - Running DU Agent healthcheck "
"'/usr/bin/AducIotAgent -h' ...")
ret_code = run_cmd('/usr/bin/AducIotAgent -h')
if ret_code != 0:
return ret_code
return 0
def download_file(url: str, target_dir: str) -> int:
parsed_url = urlparse(url)
last_slash = parsed_url.path.rindex('/')
if last_slash == -1:
print(f"download_file - No '/' found in parsed URL of '{url}'.")
return 1
filename = parsed_url.path[last_slash + 1:]
target_path = os.path.join(target_dir, filename)
if Verbose:
print(f"Downloading URL '{url}' to '{target_path}' ...")
req = requests.get(url)
with open(target_path, 'wb') as file:
file.write(req.content)
return 0
def copy_deviceupdate_agent_files(cfg_duagent, work_dir) -> int:
"""Copy files for deviceupdate agent into work dir"""
# Copy agent debian package
src_file = cfg_duagent['package_path']
if not os.path.exists(src_file):
print("config deviceupdate_agent.package_path "
f"'{src_file}' does not exist.")
return 1
try:
shutil.copy(src_file, work_dir)
# Copy DO release tarball for Ubuntu 18.04
deps1804 = cfg_duagent['dependencies']['ubuntu18.04_x64']
download_file(
url=deps1804['delivery_optimization_tarball_url'],
target_dir=work_dir
)
# Copy DO release tarball for Ubuntu 20.04
deps2004 = cfg_duagent['dependencies']['ubuntu20.04_x64']
download_file(
url=deps2004['delivery_optimization_tarball_url'],
target_dir=work_dir
)
except Exception as ex:
print(ex)
return 2
return 0
def copy_feature_deltaupdate_files(cfg_deltaupdate, work_dir):
"""Copy files for delta update feature into work_dir"""
deltaupdate_files_zip = cfg_deltaupdate['deltaupdate_files']
if not os.path.exists(deltaupdate_files_zip):
print("config deltaupdate_files "
f"'{deltaupdate_files_zip}' does not exist.")
return 1
try:
shutil.copy(deltaupdate_files_zip, work_dir)
except Exception as ex:
print(ex)
return 2
return 0
def copy_feature_deltaupdate_swupdate_files(cfg_deltaupdate, work_dir):
"""Copy custom swupdate-related files into work_dir"""
swupdate_tarball = cfg_deltaupdate['deltaupdate_swupdate_files']
if not os.path.exists(swupdate_tarball):
print("config swupdate_tarball "
f"'{swupdate_tarball}' does not exist.")
return 1
try:
shutil.copy(swupdate_tarball, work_dir)
except Exception as ex:
print(ex)
return 2
return 0
def create_bootstrap_bundle(
name: str,
config: str,
out_dir: str, work_dir: str
) -> int:
"""Create bootstrap bundle of given name using the config."""
global Verbose
print(
f"{os.linesep}Creating bootstrap bundle '{name}.tar.gz'"
f"{os.linesep} using config '{config}' ..."
)
# First, validate the example config json against the jsonschema.
is_valid, msg = validate_config(get_config_example())
if not is_valid:
print(msg)
return 1
if not os.path.exists(config):
print(f"config '{config}' does not exist.")
return 1
mkdirs_if_not_exist(out_dir)
mkdirs_if_not_exist(work_dir)
with open(config, 'r') as config_file:
config_json = json.load(config_file)
if Verbose:
print(json.dumps(config_json, indent=4))
# Copy files into work_dir
copy_deviceupdate_agent_files(config_json['deviceupdate_agent'], work_dir)
copy_feature_deltaupdate_files(
config_json['features']['delta_update'],
work_dir
)
copy_feature_deltaupdate_swupdate_files(
config_json['features']['delta_update'],
work_dir
)
# copy bootstrap scripts into bundle as well
script_dir = os.path.dirname(sys.argv[0])
try:
shutil.copy(sys.argv[0], work_dir)
shutil.copy(os.path.join(script_dir, "bootstrap.sh"), work_dir)
except Exception as ex:
print(ex)
return 2
# Create bootstrap bundle tarball
output_bundle_filepath = os.path.join(out_dir, f"{name}.tar.gz")
if Verbose:
print("Bundling following files into {output_bundle_filepath} ...")
return run_cmd(f"cd {work_dir} && tar -czvf {output_bundle_filepath} ./*")
def main() -> int:
"""Either create bootstrap bundle or bootstrap the system."""
# parse cmdline arguments
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description="Creates bootstrap bundle or bootstraps the system.",
epilog='''Example usage:
---Create bootstrap bundle---
./bootstrap.sh \\
--bundle-name bundle_name \\
--config /tmp/adu/boot/bootstrap_config.json \\
--out-dir /tmp/adu/boot/out/ \\
--work-dir /tmp/adu/boot/work/
---Run Bootstrap---
./bootstrap.sh --install
''')
parser.add_argument('-i', '--install', action='store_true',
help="Installs the bootstrap bundle to the system.")
parser.add_argument('-n', '--bundle-name', nargs='?',
help="The bootstrap bundle name.")
parser.add_argument('-c', '--config', nargs='?',
help="The path to the config file.")
parser.add_argument('-o', '--out-dir', nargs='?',
help="The output dir for the bundle tarball.")
parser.add_argument('-w', '--work-dir', nargs='?',
help="The work dir for bundle creation.")
parser.add_argument('-v', '--verbose', action='store_true',
help="Enables verbose tracing.")
args = parser.parse_args()
if args.verbose:
global Verbose
Verbose = True
ret_val = 1
if args.bundle_name is None \
and args.config is None \
and args.out_dir is None \
and args.work_dir is None \
and args.install is True:
ret_val = bootstrap_system()
elif args.bundle_name is not None \
and args.config is not None \
and args.out_dir is not None \
and args.work_dir is not None:
ret_val = create_bootstrap_bundle(
args.bundle_name,
args.config,
args.out_dir,
args.work_dir
)
else:
parser.print_help()
print(f"Completed with return code {ret_val}.")
return ret_val
if __name__ == '__main__':
sys.exit(main())