Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
parkpow
GitHub Repository: parkpow/deep-license-plate-recognition
Path: blob/master/stream/stream_light_update/main.py
643 views
1
import argparse
2
import hashlib
3
import tarfile
4
import tempfile
5
from pathlib import Path
6
7
from docker.client import DockerClient
8
9
client = DockerClient.from_env()
10
11
paths_to_copy = [
12
Path("/app"),
13
Path("/usr/local/lib/python3.8/site-packages/"),
14
Path("/usr/local/lib/python3.8/dist-packages/"),
15
]
16
CONTAINER_STOP_TIMEOUT = 2
17
18
19
def get_versions(image) -> tuple[str | None, str | None]:
20
"""
21
Returns python and stream version from container config ENV
22
:param image:
23
:return:
24
"""
25
py_version = None
26
stream_version = None
27
config = client.api.inspect_image(image)["Config"]
28
for env in config["Env"]:
29
name, value = env.split("=", maxsplit=1)
30
if name == "PYTHON_VERSION":
31
py_version = value
32
elif name == "TAG":
33
stream_version = value
34
35
return py_version, stream_version
36
37
38
def archive_image_updates(image, output) -> tuple[str | None, str | None]:
39
container = None
40
try:
41
container = client.containers.run(
42
image, command="/bin/bash", tty=True, detach=True, remove=True
43
)
44
45
for path in paths_to_copy:
46
zip_file = f"{output}/{path.name}.tar"
47
with open(zip_file, "wb") as fp:
48
bits, stat = container.get_archive(path)
49
print(stat)
50
for chunk in bits:
51
fp.write(chunk)
52
print(f"Successfully created {zip_file}")
53
return get_versions(image)
54
finally:
55
if container is not None:
56
container.stop(timeout=CONTAINER_STOP_TIMEOUT)
57
58
59
def hash_file(filepath):
60
"""Calculate the SHA256 hash of a file."""
61
hasher = hashlib.sha256()
62
with open(filepath, "rb") as f:
63
while chunk := f.read(8192):
64
hasher.update(chunk)
65
return hasher.hexdigest()
66
67
68
def extract_tar(tar_path: Path, extract_to: Path):
69
"""Extract tar file to a specified directory."""
70
with tarfile.open(tar_path, "r") as tar:
71
tar.extractall(extract_to)
72
73
74
def create_diff_tar(source_tar, destination_tar, output_tar):
75
"""Create a tar file containing only new or updated files from source_tar compared to destination_tar."""
76
with tempfile.TemporaryDirectory() as tmpdirname:
77
temp_dir = Path(tmpdirname)
78
source_dir = temp_dir / "source"
79
source_dir.mkdir(exist_ok=True)
80
destination_dir = temp_dir / "destination"
81
destination_dir.mkdir(exist_ok=True)
82
83
extract_tar(source_tar, source_dir)
84
extract_tar(destination_tar, destination_dir)
85
86
diff_files = []
87
88
for root, _, files in source_dir.walk(on_error=print):
89
for file in files:
90
source_file_path = root / file
91
relative_path = source_file_path.relative_to(source_dir)
92
destination_file_path = destination_dir / relative_path
93
94
if not destination_file_path.exists() or hash_file(
95
source_file_path
96
) != hash_file(destination_file_path):
97
diff_files.append(relative_path)
98
99
with tarfile.open(output_tar, "w") as tar:
100
for file in diff_files:
101
full_path = source_dir / file
102
tar.add(full_path, arcname=file)
103
104
105
def extract_updates(args):
106
with tempfile.TemporaryDirectory() as src, tempfile.TemporaryDirectory() as dest:
107
source_image_fs = Path(src)
108
dest_image_fs = Path(dest)
109
# Download Source Image Files
110
source_python, source_stream = archive_image_updates(
111
args.source, source_image_fs
112
)
113
# Download Source Image Files
114
dest_python, dest_stream = archive_image_updates(args.dest, dest_image_fs)
115
if source_python != dest_python:
116
print(
117
"WARNING! Update across different python version is not guaranteed to work."
118
f"You are updating from [{source_python}] to [{dest_python}]"
119
)
120
diff_fs = args.output / f"update_{dest_stream}_to_{source_stream}"
121
diff_fs.mkdir(exist_ok=True, parents=True)
122
123
for path in paths_to_copy:
124
create_diff_tar(
125
source_image_fs / f"{path.name}.tar",
126
dest_image_fs / f"{path.name}.tar",
127
diff_fs / f"{path.name}.tar",
128
)
129
130
131
def restore_updates(args):
132
container = None
133
try:
134
container = client.containers.run(
135
args.dest,
136
command="/bin/bash",
137
tty=True,
138
detach=True,
139
remove=True,
140
working_dir="/",
141
)
142
for path in paths_to_copy:
143
# TODO Delete no-longer existing files from container
144
update_file = f"{path.name}.tar"
145
with open(args.source / update_file, "rb") as fp:
146
container.put_archive(str(path.parent), fp.read())
147
# Copy source image CMD, entrypoint ENV
148
image_config = client.api.inspect_image(args.dest)["Config"]
149
container.commit(
150
"platerecognizer/alpr-stream", args.output, pause=True, conf=image_config
151
)
152
print(f"Updated image is platerecognizer/alpr-stream:{args.output}")
153
finally:
154
if container is not None:
155
container.stop(timeout=CONTAINER_STOP_TIMEOUT)
156
157
158
def main():
159
parser = argparse.ArgumentParser(
160
prog="stream-sdk-update", description="Stream SDK Slight Update"
161
)
162
subparsers = parser.add_subparsers(help="Extract/Restore Help")
163
parser_a = subparsers.add_parser("extract", help="Extract updates on machine A")
164
parser_a.add_argument(
165
"-s", "--source", type=str, help="Docker Image Tag with updates"
166
)
167
parser_a.add_argument("-d", "--dest", type=str, help="Docker Image Tag to update")
168
parser_a.add_argument("-o", "--output", type=Path, help="Folder to extract to")
169
parser_a.set_defaults(func=extract_updates)
170
171
parser_b = subparsers.add_parser("restore", help="Restore on machine B")
172
parser_b.add_argument("-s", "--source", type=Path, help="Folder to restore from")
173
parser_b.add_argument("-d", "--dest", type=str, help="Existing image Tag to update")
174
parser_b.add_argument(
175
"-o",
176
"--output",
177
type=str,
178
help="New Docker Image Tag With the Updates",
179
default="latest",
180
)
181
parser_b.set_defaults(func=restore_updates)
182
args = parser.parse_args()
183
args.func(args)
184
185
186
if __name__ == "__main__":
187
main()
188
189