Path: blob/master/stream/stream_light_update/main.py
643 views
import argparse1import hashlib2import tarfile3import tempfile4from pathlib import Path56from docker.client import DockerClient78client = DockerClient.from_env()910paths_to_copy = [11Path("/app"),12Path("/usr/local/lib/python3.8/site-packages/"),13Path("/usr/local/lib/python3.8/dist-packages/"),14]15CONTAINER_STOP_TIMEOUT = 2161718def get_versions(image) -> tuple[str | None, str | None]:19"""20Returns python and stream version from container config ENV21:param image:22:return:23"""24py_version = None25stream_version = None26config = client.api.inspect_image(image)["Config"]27for env in config["Env"]:28name, value = env.split("=", maxsplit=1)29if name == "PYTHON_VERSION":30py_version = value31elif name == "TAG":32stream_version = value3334return py_version, stream_version353637def archive_image_updates(image, output) -> tuple[str | None, str | None]:38container = None39try:40container = client.containers.run(41image, command="/bin/bash", tty=True, detach=True, remove=True42)4344for path in paths_to_copy:45zip_file = f"{output}/{path.name}.tar"46with open(zip_file, "wb") as fp:47bits, stat = container.get_archive(path)48print(stat)49for chunk in bits:50fp.write(chunk)51print(f"Successfully created {zip_file}")52return get_versions(image)53finally:54if container is not None:55container.stop(timeout=CONTAINER_STOP_TIMEOUT)565758def hash_file(filepath):59"""Calculate the SHA256 hash of a file."""60hasher = hashlib.sha256()61with open(filepath, "rb") as f:62while chunk := f.read(8192):63hasher.update(chunk)64return hasher.hexdigest()656667def extract_tar(tar_path: Path, extract_to: Path):68"""Extract tar file to a specified directory."""69with tarfile.open(tar_path, "r") as tar:70tar.extractall(extract_to)717273def create_diff_tar(source_tar, destination_tar, output_tar):74"""Create a tar file containing only new or updated files from source_tar compared to destination_tar."""75with tempfile.TemporaryDirectory() as tmpdirname:76temp_dir = Path(tmpdirname)77source_dir = temp_dir / "source"78source_dir.mkdir(exist_ok=True)79destination_dir = temp_dir / "destination"80destination_dir.mkdir(exist_ok=True)8182extract_tar(source_tar, source_dir)83extract_tar(destination_tar, destination_dir)8485diff_files = []8687for root, _, files in source_dir.walk(on_error=print):88for file in files:89source_file_path = root / file90relative_path = source_file_path.relative_to(source_dir)91destination_file_path = destination_dir / relative_path9293if not destination_file_path.exists() or hash_file(94source_file_path95) != hash_file(destination_file_path):96diff_files.append(relative_path)9798with tarfile.open(output_tar, "w") as tar:99for file in diff_files:100full_path = source_dir / file101tar.add(full_path, arcname=file)102103104def extract_updates(args):105with tempfile.TemporaryDirectory() as src, tempfile.TemporaryDirectory() as dest:106source_image_fs = Path(src)107dest_image_fs = Path(dest)108# Download Source Image Files109source_python, source_stream = archive_image_updates(110args.source, source_image_fs111)112# Download Source Image Files113dest_python, dest_stream = archive_image_updates(args.dest, dest_image_fs)114if source_python != dest_python:115print(116"WARNING! Update across different python version is not guaranteed to work."117f"You are updating from [{source_python}] to [{dest_python}]"118)119diff_fs = args.output / f"update_{dest_stream}_to_{source_stream}"120diff_fs.mkdir(exist_ok=True, parents=True)121122for path in paths_to_copy:123create_diff_tar(124source_image_fs / f"{path.name}.tar",125dest_image_fs / f"{path.name}.tar",126diff_fs / f"{path.name}.tar",127)128129130def restore_updates(args):131container = None132try:133container = client.containers.run(134args.dest,135command="/bin/bash",136tty=True,137detach=True,138remove=True,139working_dir="/",140)141for path in paths_to_copy:142# TODO Delete no-longer existing files from container143update_file = f"{path.name}.tar"144with open(args.source / update_file, "rb") as fp:145container.put_archive(str(path.parent), fp.read())146# Copy source image CMD, entrypoint ENV147image_config = client.api.inspect_image(args.dest)["Config"]148container.commit(149"platerecognizer/alpr-stream", args.output, pause=True, conf=image_config150)151print(f"Updated image is platerecognizer/alpr-stream:{args.output}")152finally:153if container is not None:154container.stop(timeout=CONTAINER_STOP_TIMEOUT)155156157def main():158parser = argparse.ArgumentParser(159prog="stream-sdk-update", description="Stream SDK Slight Update"160)161subparsers = parser.add_subparsers(help="Extract/Restore Help")162parser_a = subparsers.add_parser("extract", help="Extract updates on machine A")163parser_a.add_argument(164"-s", "--source", type=str, help="Docker Image Tag with updates"165)166parser_a.add_argument("-d", "--dest", type=str, help="Docker Image Tag to update")167parser_a.add_argument("-o", "--output", type=Path, help="Folder to extract to")168parser_a.set_defaults(func=extract_updates)169170parser_b = subparsers.add_parser("restore", help="Restore on machine B")171parser_b.add_argument("-s", "--source", type=Path, help="Folder to restore from")172parser_b.add_argument("-d", "--dest", type=str, help="Existing image Tag to update")173parser_b.add_argument(174"-o",175"--output",176type=str,177help="New Docker Image Tag With the Updates",178default="latest",179)180parser_b.set_defaults(func=restore_updates)181args = parser.parse_args()182args.func(args)183184185if __name__ == "__main__":186main()187188189