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())