wadebug/wa_actions/docker_utils.py (174 lines of code) (raw):

# Copyright (c) Facebook, Inc. and its affiliates. # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. from __future__ import absolute_import, division, print_function, unicode_literals import os import tarfile import tempfile from datetime import datetime, timedelta from enum import Enum import docker from six import BytesIO from wadebug.wa_actions.models.wa_container import WAContainer WA_WEBAPP_CONTAINER_TAG = "whatsapp.biz/web" WA_COREAPP_CONTAINER_TAG = "whatsapp.biz/coreapp" MYSQL_CONTAINER_TAG = "mysql" CONTAINER_RUNNING = "running" DATE_FORMAT = "%Y-%m-%d %H:%M:%S" EXPIRED_DATE_FORMAT = "%Y-%m-%d" LIFETIME_OF_BETA_BUILD_IN_DAYS = 45 LIFETIME_OF_BUILD_IN_DAYS = 180 TEMP_TAR_FILENAME = "temp.tar" def get_all_containers(): client = docker.from_env() return client.containers.list(all=True) def get_container_logs(container, since_datetime=None, until_datetime=None): return container.logs(since=since_datetime, until=until_datetime) def get_inspect_result(container): client = docker.from_env() res = client.api.inspect_container(container.short_id) # hide password for index, value in enumerate(res["Config"]["Env"]): if value.lower().find("password") != -1: arr = value.split("=") res["Config"]["Env"][index] = arr[0] + "*******" return res def get_core_dump_logs(container): client = docker.from_env() files_changed = client.api.diff(container.short_id) coredump_logs = [] for file_change in files_changed: file_change_type = file_change["Kind"] full_path = file_change["Path"] # the crash files should have names like: # /usr/local/waent/logs/wa-service-bffb11a7-crash.log if ( "-crash.log" in full_path and file_change_type != DockerDiffFileChange.DELETED ): folder_path, file_name = full_path.rsplit("/", 1) coredump = get_archive_from_container(container, folder_path, file_name) coredump_logs.append(coredump) result = "\n".join(coredump_logs) return result def get_archive_from_container(container, path, file_name): with tempfile.NamedTemporaryFile() as destination: stream, stat = container.get_archive(os.path.join(path, file_name)) for data in stream: destination.write(data) destination.seek(0) retrieved_data = untar_file(destination, file_name) retrieved_data = retrieved_data.decode("utf-8") return retrieved_data # https://docker-py.readthedocs.io/en/1.5.0/api/#get_archive # 'docker sdk' for get-archive returns tuple. In which, first element is a raw tar data stream. # We should untar this file to read content. # The reason it gives tar file is path to get_archive() can be a folder instead of just a file def untar_file(tardata, file_name): with tarfile.open(mode="r", fileobj=tardata) as t: f = t.extractfile(file_name) result = f.read() f.close() return result def put_archive_to_container(container, src, dest): with tempfile.NamedTemporaryFile() as temptar: # tempfile backed tarfile file_data = open(src, "rb").read() tar_file = tarfile.open(fileobj=temptar, mode="w") tarinfo = tarfile.TarInfo(name=os.path.basename(src)) tarinfo.size = os.stat(src).st_size tar_file.addfile(tarinfo, BytesIO(file_data)) tar_file.close() temptar.flush() temptar.seek(0) container.put_archive(dest, temptar.read()) def write_to_file_in_binary(path, content): with open(path, "wb") as file: file.write(content) file.close() def write_to_file(path, content): with open(path, "w") as file: file.write(content) file.close() def get_wa_version_from_container(container): return get_version(container.image.attrs["RepoTags"][0].split(":")[1]) def get_running_wacore_containers(): return [c.container for c in get_running_wa_containers() if c.is_coreapp()] def get_running_waweb_containers(): return [c.container for c in get_running_wa_containers() if c.is_webapp()] def get_running_wa_containers(): return [c for c in get_wa_containers() if c.is_running()] def get_wa_containers(): """Return all probably relevant containers, including MySQL, if exists.""" return [WAContainer(c) for c in get_all_containers() if is_wa_container(c)] def is_wa_container(container): return ( len( [ repo_tag for repo_tag in container.image.attrs["RepoTags"] if ( repo_tag.find(WA_WEBAPP_CONTAINER_TAG) > -1 or repo_tag.find(WA_COREAPP_CONTAINER_TAG) > -1 or repo_tag.find(MYSQL_CONTAINER_TAG) > -1 ) ] ) > 0 ) def is_container_running(container): return container.status == CONTAINER_RUNNING def get_all_running_wa_containers_except_db(): return [c.container for c in get_running_wa_containers() if not c.is_db()] def get_mysql_password(wa_container): client = docker.from_env() res = client.api.inspect_container(wa_container.container.short_id) for _, value in enumerate(res["Config"]["Env"]): if value.find("MYSQL_ROOT_PASSWORD") != -1: arr = value.split("=") return arr[1] return "" def get_value_by_inspecting_container_environment(container, key_in_config): client = docker.from_env() res = client.api.inspect_container(container.short_id) for _, value in enumerate(res["Config"]["Env"]): if value.find(key_in_config) != -1: arr = value.split("=") return arr[1] return "" def get_container_port_bindings(container): return container.attrs["HostConfig"]["PortBindings"] def is_beta_build(version_number): return int(version_number) % 2 == 0 def get_version(version): """ It returns the version in format of v2.{major version}.{minor version}. All beta builds are marked 2.{even_number}.*. Expiry date for these is image create date + 45 days All external builds are 2.{odd_number}.*. Expiry date for all 2.{odd_number}.* is same. Although 2.19.* is for external users, expiry date for these builds are image_create_date + 180 days So, this function returns if version >= v2.21.1 or if it's not a internal build (v2.{major version}, v2.{major version}.{minor version}) Ex : i.e for 2.21.1 it returns (v2.21,v2.21.1) otherwise (v2.{major version}.{minor version}, v2.{major version}.{minor version}) Ex : i.e for 2.20.2 it returns (v2.20.2,v2.20.2) """ arr = version.split(".") if is_beta_build(arr[1]) or int(arr[1]) <= 19: return (version, version) return (arr[0] + "." + arr[1], version) def get_expiration_map(): def is_wa_image(image): if not image["RepoDigests"]: return False for repo_tag in image["RepoDigests"]: return ( repo_tag.find(WA_WEBAPP_CONTAINER_TAG) > -1 or repo_tag.find(WA_COREAPP_CONTAINER_TAG) > -1 ) client = docker.from_env() images = client.api.images() expiration_map = {} for image in images: if is_wa_image(image): ver = get_version(image["RepoTags"][0].split(":")[1]) if ver[0] not in expiration_map: if "Labels" in image and "EXPIRES_ON" in image["Labels"]: dt_ts = datetime.strptime( image["Labels"]["EXPIRES_ON"], EXPIRED_DATE_FORMAT ) expiration_map[ver[0]] = str(dt_ts) continue ts = get_expiry_date(ver[0], image["Created"]) expiration_map[ver[0]] = ts return expiration_map def get_expiry_date(version, ts): arr = version.split(".") str_ts = datetime.utcfromtimestamp(ts).strftime(DATE_FORMAT) dt_ts = datetime.strptime(str_ts, DATE_FORMAT) expiry_date = "" if is_beta_build(arr[1]): expiry_date = dt_ts + timedelta(LIFETIME_OF_BETA_BUILD_IN_DAYS) else: expiry_date = dt_ts + timedelta(LIFETIME_OF_BUILD_IN_DAYS) return str(expiry_date) class DockerDiffFileChange(Enum): CREATED = 0 MODIFIED = 1 DELETED = 2