代码拉取完成,页面将自动刷新
#!/usr/bin/env python3
'''
Author: Yanrui Hu
Date: 2023/5/22
Description: 使用docker模块进行自动化容器创建与容器内脚本执行、上传文件至容器、commit容器、删除容器、push容器等操作
Usage: 直接运行此脚本,会打印出帮助信息
Keywords: docker, python, automate, container
'''
import docker, os, subprocess, sys, argparse, logging
DESCRIPTION = """通过指定镜像启动容器, 将本地文件上传到容器中, 在容器中执行bash命令,
并将容器commit得到镜像, 最后推送到本地镜像仓库。
注意:有几点需要说明:
1. 如果没有安装docker, 会自动下载并安装docker
2. 如果执行此脚本的用户没有root权限, 会自动创建docker用户组, 并将当前用户加入docker用户组
3. 如果没有启动本地镜像仓库, 会自动启动本地镜像仓库 (通过提供-l来指定)
4. -i IMAGE 对应于 docker run 的参数
5. -o OUT_IMAGE, -a AUTHOR, -m MESSAGE 对应于 docker commit 的参数
6. -s SRC, -d DEST 对应于 docker cp 的参数
8. -c CMD, -u USER, -w WORKDIR 对应于 docker exec 的参数
"""
EPILOG = """Example:
python3 automate.py -i ubuntu:20.04 -o localhost:5000/ubuntu:v1.0 \
-c cmd_file.txt\
-a 'HYR <yanruinku@qq.com>' -m "My ubuntu image, version 1.0"\
-l
# Tips: 打开此文件,阅读头部的注释,可以了解更多细节
"""
# ------------------------------------------------------------
# 配置日志
# ------------------------------------------------------------
# 创建一个logger
running_logger = logging.getLogger("running")
running_logger.setLevel(logging.DEBUG)
# 创建一个handler,用于写入日志文件
file_handler = logging.FileHandler("running.log")
file_handler.setLevel(logging.DEBUG)
# 设置日志格式
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d %(funcName)s - %(message)s'
)
file_handler.setFormatter(formatter)
# 给logger添加handler
running_logger.addHandler(file_handler)
def get_all_containers_status():
"""Get all containers' status"""
client = docker.from_env()
for container in client.containers.list(all=True):
print(f'container: {container.short_id} Status: {container.status}')
def get_client_events():
"""Get real-time events from the server. Similar to the `docker events` command."""
client = docker.from_env()
for event in client.events(decode=True):
print(event)
def run_and_exec(image: str, user: str, workdir: str, cmd_file: str) -> str:
"""通过给定的镜像启动一个容器,并执行 cmd_file 中的命令, 返回容器的短ID
Params:
- image: 镜像名
- user: 容器内执行命令的用户
- workdir: 容器内执行命令的工作目录
- cmd_file: 存放命令的文件,可以是 .txt 或 .sh 文件
Return: 容器的短ID
"""
running_logger.info(f"run_and_exec: {locals()}")
client = docker.from_env()
container = client.containers.run(
image=image,
command='/bin/bash', # 启动一个bash shell 可以一直等待下去,适合让容器保持后台执行不退出
detach=True,
tty=True, # 分配一个伪终端,一定要设置为True,否则,容器进程会自动结束,从而容器被remove
stdin_open=False, # 不要打开标准输入! 否则,容器进程从stdin读不到数据会自动结束,从而容器被remove
remove=True,
network='host', # 用于配置网络
) # 启动一个容器在后台运行,备用
print(f'Container {container.short_id} started.')
running_logger.info(f'Container {container.short_id} started.')
# 判断 cmd_file 是否存在
if not os.path.exists(cmd_file):
print(f"Error6: {cmd_file} not found.")
exit(1)
running_logger.info(f"cmd_file: {cmd_file}")
# 判断 cmd_file 的后缀名是 txt 还是 sh
if cmd_file.endswith(".txt"):
running_logger.info(f"==== Starting shell commands execution ====")
with open(cmd_file, "r") as f:
for cmd in f.readlines():
cmd = cmd.strip()
# 如果读到文件末尾,则退出
if not cmd or cmd.startswith("#"):
continue
running_logger.info(f"cmd: {cmd}")
print(f"{user}@:{container.short_id}# cmd")
running_logger.info(f"{user}@:{container.short_id}# {cmd}")
command = ["/bin/bash", "-xc", cmd] # TODO: 或许将来会修改这里的参数
try:
""" 这是老代码,脚本执行过程中的信息不能及时看到,不好用
res = container.exec_run(command, user=user, workdir=workdir)
running_logger.info(f"res.output: {res.output.decode()}")
print(f"命令在容器内执行结果的输出:\n{res.output.decode()}")
running_logger.info(f"res.exit_code: {res.exit_code}")
print(f"命令在容器内执行的exit code: {res.exit_code}") """
res = subprocess.run(f'docker exec -it {container.short_id}'+ " ".join(command), shell=True)
exit_code = res.returncode
running_logger.info(f"Exitcode of {cmd} is: {exit_code}")
except docker.errors.APIError as e:
running_logger.info(f"Exception: {e}")
print(f"Exception: {e}")
exit(2)
# 如果commmand执行的返回值不为0,则打印错误信息并退出
assert exit_code == 0, f"Error2: exit_code不为0 请检查命令是否正确"
running_logger.info(f"==== Ending shell commands execution ====")
elif cmd_file.endswith(".sh"):
# 将 cmd_file.sh 打包成 cmd_file.tar之后 上传到容器内
cmd = f"tar -cf cmd_file.tar '{cmd_file}'"
assert subprocess.run(cmd, shell=True).returncode == 0
running_logger.info(f"Succeed: 打包文件 {cmd_file} 成 cmd_file.tar 成功")
with open("cmd_file.tar", "rb") as f:
assert container.put_archive("/root", f.read()), "Error5: 上传文件cmd_file.tar失败"
running_logger.info(f"Succeed: 上传文件cmd_file.tar成功")
# 清理本地的 cmd_file.tar
cmd = "rm -rf cmd_file.tar"
assert subprocess.run(cmd, shell=True).returncode == 0
running_logger.info(f"Succeed: 清理本地的 cmd_file.tar 成功")
running_logger.info(f"==== Starting shell script execution ====")
command = ["/bin/bash", '-x', f"'/root/{cmd_file}'"] # TODO: 或许将来会修改这里的bash参数
try:
""" 这是老代码,脚本执行过程中的信息不能及时看到,不好用
res = container.exec_run(command, user=user, workdir=workdir)
running_logger.info(f"res.output: {res.output.decode()}")
print(f"脚本在容器内执行结果的输出:\n{res.output.decode()}")
running_logger.info(f"res.exit_code: {res.exit_code}")
print(f"脚本在容器内执行的exit code: {res.exit_code}") """
# 新代码
res = subprocess.run(f"docker exec -it {container.short_id} /bin/bash -x '/root/{cmd_file}'", shell=True)
exit_code = res.returncode
running_logger.info(f"Exitcode of {cmd_file} is: {exit_code}")
except docker.errors.APIError as e:
running_logger.info(f"Exception: {e}")
print(f"Exception: {e}")
exit(3)
# 如果commmand执行的返回值不为0,则打印错误信息并退出
assert exit_code == 0, f"Error3: exit_code不为0 请检查脚本是否有错误"
running_logger.info(f"==== Ending shell commands execution ====")
else:
running_logger.info(f"Error4: {cmd_file} is not a .txt or .sh file.")
print(f"Error4: {cmd_file} is not a .txt or .sh file.")
exit(4)
running_logger.info(f"Succeed: {container.short_id} 执行 {cmd_file}成功")
print(f"Command_file {cmd_file} executed completely.")
return container.short_id
def put_file_to_container(container_id: str, src: str, dest: str):
"""将本地文件 src 上传到容器 container_id 中的 dst 目录下
Params:
- container_id: 容器的短ID
- src: 本地文件的路径
- dst: 容器内的目标路径
"""
running_logger.info(f"put_file_to_container: {locals()}")
client = docker.from_env()
container = client.containers.get(container_id)
# 判断 src 是否存在
if not os.path.exists(src):
running_logger.info(f"Error7: {src} not found.")
print(f"Error7: {src} not found.")
exit(4)
# 必须先将 src 打包成 tar 文件,再上传到容器中
assert subprocess.run(f'tar -cf src.tar {src}', shell=True).returncode == 0
with open("src.tar", "rb") as f:
assert container.put_archive(dest, f.read()), "Error8: 上传文件src.tar失败"
# 清理本地的 src.tar
assert subprocess.run("rm src.tar", shell=True).returncode == 0
running_logger.info(f"Succeed: 上传文件 {src} 到容器 {container_id} 的 {dest} 成功")
print(f"File {src} uploaded to {dest}.")
def commit_and_push(container_id: str, out_image: str, author: str, message: str):
"""将容器commit得到镜像out_image, 并推送到镜像仓库
Params:
- container_id: 容器的短ID
- out_image: 输出的镜像名
- author: 镜像作者
- message: 镜像的描述信息
"""
running_logger.info(f"commit_and_push: {locals()}")
client = docker.from_env()
container = client.containers.get(container_id)
repo, tag = out_image.rsplit(":", 1)
# 将容器commit得到镜像
# container.commit(repo, tag, author=author, message=message)
res = subprocess.run(f"docker commit -a='{author}' -m='{message}' {container_id} '{out_image}'", shell=True)
assert res.returncode == 0, f"Error9: 镜像 {out_image} commit失败"
running_logger.info(f"Succeed: 镜像 {out_image} commit成功")
print(f"Image {out_image} commited.")
# 停掉容器
container.stop() # run 时,指定了 remove=True,所以这里不需要手动删除容器
# 将镜像推送到镜像仓库
# res = client.api.push(repo, tag, stream=False, decode=True)
# running_logger.info(f"res: {res}")
res = subprocess.run(f"docker push {out_image}", shell=True)
assert res.returncode == 0, f"Error10: 镜像 {out_image} push失败"
print(f"Image {out_image} pushed.")
running_logger.info(f"Succeed: 镜像 {out_image} push成功")
def ensure_docker():
"""确保 docker的存在, 下载并安装 docker"""
cmd1 = "curl -fsSL get.docker.com -o get-docker.sh"
cmd2 = "sh get-docker.sh --mirror Aliyun"
if os.geteuid() != 0:
cmd2 = "sudo " + cmd2
print("正在检测 docker 是否已安装……")
res = subprocess.run("which docker", shell=True, capture_output=True)
if res.returncode != 0:
print("docker 尚未安装,正在安装……")
res = subprocess.run(cmd1, shell=True, capture_output=True)
assert res.returncode == 0, print(f"{cmd1}失败! 请自行下载安装脚本")
res = subprocess.run(cmd2, shell=True, capture_output=True)
assert res.returncode == 0, print(f"{cmd2}失败! 请手动排查错误并安装")
print("docker 安装成功 ✅")
else:
print("docker 已安装 ✅")
# 检查docker后台服务是否可以使用
cmd = "docker info > /dev/null 2>&1"
res = subprocess.run(cmd, shell=True, capture_output=True)
if res.returncode == 0:
return
# 启动 docker
cmd3 = "systemctl enable docker"
cmd4 = "systemctl start docker"
if os.geteuid() != 0:
cmd3 = "sudo " + cmd3
cmd4 = "sudo " + cmd4
print("正在启动 Docker 后台服务")
res = subprocess.run(cmd3, shell=True, capture_output=True)
assert res.returncode == 0, print(f"{cmd3}失败! 请手动排查错误")
res = subprocess.run(cmd4, shell=True, capture_output=True)
assert res.returncode == 0, print(f"{cmd4}失败! 请手动排查错误")
def create_docker_grp():
"""为非 root 用户创建 docker 用户组, 并加入其中"""
if os.geteuid() == 0:
running_logger.info("当前用户为 root 用户,无需创建 docker 用户组")
return # root 用户不必创建 docker 用户组
cmd1 = "sudo groupadd docker"
cmd2 = "sudo usermod -aG docker $USER"
# res = os.popen("cat /etc/group | grep docker").read()
cmd = "cat /etc/group | grep docker"
res = subprocess.run(cmd, shell=True, capture_output=True)
assert res.returncode == 0, print(f"{cmd}失败! 请手动排查错误")
# 检测 /etc/group 文件 是否有 docker 组
if "docker" in res.stdout.decode():
print("docker 用户组已存在 ✅")
else:
res1 = subprocess.run(cmd1, shell=True, capture_output=True)
assert res1.returncode == 0, print(f"{cmd1}失败! 请手动排查错误")
del cmd1, res1
# 获取当前用户名
username = os.getlogin()
# 检测当前用户是否在 docker 组中
if username in res.stdout.decode():
print("当前登录用户已在 docker 用户组 ✅")
else:
res2 = subprocess.run(cmd2, shell=True, capture_output=True)
assert res2.returncode == 0, print(f"{cmd2}失败! 请手动排查错误")
del res2
def ensure_local_registry():
"""确保本地镜像存储库已启动"""
running_logger.info("ensure_local_registry")
cmd = "docker ps -a | grep registry"
res = subprocess.run(cmd, shell=True, capture_output=True)
if res.returncode == 0 and "Up" in res.stdout.decode():
return
elif res.returncode == 0 and "Exited" in res.stdout.decode():
cmd = "docker start registry"
res = subprocess.run(cmd, shell=True, capture_output=True)
assert res.returncode == 0, print(f"{cmd}失败! 请手动排查错误")
return
# 启动本地镜像存储库
cmd = "docker run -d -p 5000:5000 --restart=always --name registry registry"
res = subprocess.run(cmd, shell=True, capture_output=True)
assert res.returncode == 0, print(f"{cmd}失败! 请手动排查错误")
def main():
"""程序启动入口,解析命令行参数,执行相应操作"""
parser = argparse.ArgumentParser(description=DESCRIPTION, epilog=EPILOG)
parser.add_argument(
"-i",
"--image",
type=str,
required=True,
help="用于启动容器的镜像名,格式为 <仓库名>:<标签>,如 docker.io/ubuntu:latest",
)
parser.add_argument(
"-o",
"--out-image",
type=str,
required=True,
help="得到的镜像名,格式为 <仓库名>:<标签>,如 localhost:5000/ubuntu:v1.0",
)
parser.add_argument(
"-s",
"--src",
type=str,
required=False,
help="欲上传的本地文件路径,如 /home/xxx/xxx, 支持文件或目录, 必须与 -d 参数同时使用",
)
parser.add_argument(
"-d",
"--dest",
type=str,
required=False,
default="/root",
help="上传至容器内部的路径,如 /root/xxx/xxx, 必须与 -s 参数同时使用",
)
parser.add_argument(
"-c",
"--command-file",
type=str,
required=True,
help="欲在容器中执行的命令文件,可以是.txt或.sh文件",
)
parser.add_argument(
"-u",
"--user",
type=str,
required=False,
default="root",
help="容器中执行命令的用户,如 root",
)
parser.add_argument(
"-w",
"--workdir",
type=str,
required=False,
default="/root",
help="容器中执行命令的工作目录,如 /root",
)
parser.add_argument(
"-a",
"--author",
type=str,
required=False,
help="镜像作者,如 xxx",
)
parser.add_argument(
"-m",
"--message",
type=str,
required=False,
help="镜像描述信息,如 xxx",
)
parser.add_argument(
"-l",
"--local-registry",
action="store_true",
required=False,
help="启动一个本地镜像存储库",
)
if len(sys.argv) == 1: # 如果没有提供任何参数, 则打印帮助信息
parser.print_help()
sys.exit()
args = parser.parse_args()
running_logger.info(args)
print(f'{vars(args)=}')
# 确保 docker 的存在
ensure_docker()
# 为非 root 用户创建 docker 用户组, 并加入其中
create_docker_grp()
# 启动本地镜像存储库,若需要
if args.local_registry:
ensure_local_registry()
container_id = run_and_exec(
image=args.image,
user=args.user,
workdir=args.workdir,
cmd_file=args.command_file,
)
assert container_id, print("获取container_id失败! 请手动排查错误")
if args.src:
put_file_to_container(container_id=container_id, src=args.src, dst=args.dest)
# 将容器commit得到镜像,然后推送到镜像仓库
commit_and_push(container_id, args.out_image, args.author, args.message)
if __name__ == "__main__":
main()
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。