blob: fca61bb0204dee63c87650fb18298bd95decf3e5 [file] [log] [blame]
Matthias Andreas Benkardd63fe562023-11-18 13:18:30 +01001import os
2import sys
3import uvicorn
4import json
5import uuid
6import async_timeout
7import asyncio
Matthias Andreas Benkardd63fe562023-11-18 13:18:30 +01008import aiodocker
9import docker
10import logging
11from logging.config import dictConfig
12from fastapi import FastAPI, Response, Request
13from modules.DockerApi import DockerApi
Matthias Andreas Benkard9dabdab2024-03-06 13:12:09 +010014from redis import asyncio as aioredis
15from contextlib import asynccontextmanager
Matthias Andreas Benkardd63fe562023-11-18 13:18:30 +010016
17dockerapi = None
Matthias Andreas Benkard9dabdab2024-03-06 13:12:09 +010018
19@asynccontextmanager
20async def lifespan(app: FastAPI):
21 global dockerapi
22
23 # Initialize a custom logger
24 logger = logging.getLogger("dockerapi")
25 logger.setLevel(logging.INFO)
26 # Configure the logger to output logs to the terminal
27 handler = logging.StreamHandler()
28 handler.setLevel(logging.INFO)
29 formatter = logging.Formatter("%(levelname)s: %(message)s")
30 handler.setFormatter(formatter)
31 logger.addHandler(handler)
32
33 logger.info("Init APP")
34
35 # Init redis client
36 if os.environ['REDIS_SLAVEOF_IP'] != "":
37 redis_client = redis = await aioredis.from_url(f"redis://{os.environ['REDIS_SLAVEOF_IP']}:{os.environ['REDIS_SLAVEOF_PORT']}/0")
38 else:
39 redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0")
40
41 # Init docker clients
42 sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
43 async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock')
44
45 dockerapi = DockerApi(redis_client, sync_docker_client, async_docker_client, logger)
46
47 logger.info("Subscribe to redis channel")
48 # Subscribe to redis channel
49 dockerapi.pubsub = redis.pubsub()
50 await dockerapi.pubsub.subscribe("MC_CHANNEL")
51 asyncio.create_task(handle_pubsub_messages(dockerapi.pubsub))
52
53
54 yield
55
56 # Close docker connections
57 dockerapi.sync_docker_client.close()
58 await dockerapi.async_docker_client.close()
59
60 # Close redis
61 await dockerapi.pubsub.unsubscribe("MC_CHANNEL")
62 await dockerapi.redis_client.close()
63
64app = FastAPI(lifespan=lifespan)
Matthias Andreas Benkardd63fe562023-11-18 13:18:30 +010065
66# Define Routes
67@app.get("/host/stats")
68async def get_host_update_stats():
69 global dockerapi
70
71 if dockerapi.host_stats_isUpdating == False:
72 asyncio.create_task(dockerapi.get_host_stats())
73 dockerapi.host_stats_isUpdating = True
74
75 while True:
76 if await dockerapi.redis_client.exists('host_stats'):
77 break
78 await asyncio.sleep(1.5)
79
80 stats = json.loads(await dockerapi.redis_client.get('host_stats'))
81 return Response(content=json.dumps(stats, indent=4), media_type="application/json")
82
83@app.get("/containers/{container_id}/json")
84async def get_container(container_id : str):
85 global dockerapi
86
87 if container_id and container_id.isalnum():
88 try:
89 for container in (await dockerapi.async_docker_client.containers.list()):
90 if container._id == container_id:
91 container_info = await container.show()
92 return Response(content=json.dumps(container_info, indent=4), media_type="application/json")
93
94 res = {
95 "type": "danger",
96 "msg": "no container found"
97 }
98 return Response(content=json.dumps(res, indent=4), media_type="application/json")
99 except Exception as e:
100 res = {
101 "type": "danger",
102 "msg": str(e)
103 }
104 return Response(content=json.dumps(res, indent=4), media_type="application/json")
105 else:
106 res = {
107 "type": "danger",
108 "msg": "no or invalid id defined"
109 }
110 return Response(content=json.dumps(res, indent=4), media_type="application/json")
111
112@app.get("/containers/json")
113async def get_containers():
114 global dockerapi
115
116 containers = {}
117 try:
118 for container in (await dockerapi.async_docker_client.containers.list()):
119 container_info = await container.show()
120 containers.update({container_info['Id']: container_info})
121 return Response(content=json.dumps(containers, indent=4), media_type="application/json")
122 except Exception as e:
123 res = {
124 "type": "danger",
125 "msg": str(e)
126 }
127 return Response(content=json.dumps(res, indent=4), media_type="application/json")
128
129@app.post("/containers/{container_id}/{post_action}")
130async def post_containers(container_id : str, post_action : str, request: Request):
131 global dockerapi
132
133 try :
134 request_json = await request.json()
135 except Exception as err:
136 request_json = {}
137
138 if container_id and container_id.isalnum() and post_action:
139 try:
140 """Dispatch container_post api call"""
141 if post_action == 'exec':
142 if not request_json or not 'cmd' in request_json:
143 res = {
144 "type": "danger",
145 "msg": "cmd is missing"
146 }
147 return Response(content=json.dumps(res, indent=4), media_type="application/json")
148 if not request_json or not 'task' in request_json:
149 res = {
150 "type": "danger",
151 "msg": "task is missing"
152 }
153 return Response(content=json.dumps(res, indent=4), media_type="application/json")
154
155 api_call_method_name = '__'.join(['container_post', str(post_action), str(request_json['cmd']), str(request_json['task']) ])
156 else:
157 api_call_method_name = '__'.join(['container_post', str(post_action) ])
158
159 api_call_method = getattr(dockerapi, api_call_method_name, lambda container_id: Response(content=json.dumps({'type': 'danger', 'msg':'container_post - unknown api call' }, indent=4), media_type="application/json"))
160
161 dockerapi.logger.info("api call: %s, container_id: %s" % (api_call_method_name, container_id))
162 return api_call_method(request_json, container_id=container_id)
163 except Exception as e:
164 dockerapi.logger.error("error - container_post: %s" % str(e))
165 res = {
166 "type": "danger",
167 "msg": str(e)
168 }
169 return Response(content=json.dumps(res, indent=4), media_type="application/json")
170
171 else:
172 res = {
173 "type": "danger",
174 "msg": "invalid container id or missing action"
175 }
176 return Response(content=json.dumps(res, indent=4), media_type="application/json")
177
178@app.post("/container/{container_id}/stats/update")
179async def post_container_update_stats(container_id : str):
180 global dockerapi
181
182 # start update task for container if no task is running
183 if container_id not in dockerapi.containerIds_to_update:
184 asyncio.create_task(dockerapi.get_container_stats(container_id))
185 dockerapi.containerIds_to_update.append(container_id)
186
187 while True:
188 if await dockerapi.redis_client.exists(container_id + '_stats'):
189 break
190 await asyncio.sleep(1.5)
191
192 stats = json.loads(await dockerapi.redis_client.get(container_id + '_stats'))
193 return Response(content=json.dumps(stats, indent=4), media_type="application/json")
Matthias Andreas Benkard9dabdab2024-03-06 13:12:09 +0100194
Matthias Andreas Benkardd63fe562023-11-18 13:18:30 +0100195
196# PubSub Handler
197async def handle_pubsub_messages(channel: aioredis.client.PubSub):
198 global dockerapi
199
200 while True:
201 try:
202 async with async_timeout.timeout(60):
203 message = await channel.get_message(ignore_subscribe_messages=True, timeout=30)
204 if message is not None:
205 # Parse message
206 data_json = json.loads(message['data'].decode('utf-8'))
207 dockerapi.logger.info(f"PubSub Received - {json.dumps(data_json)}")
208
209 # Handle api_call
210 if 'api_call' in data_json:
211 # api_call: container_post
212 if data_json['api_call'] == "container_post":
213 if 'post_action' in data_json and 'container_name' in data_json:
214 try:
215 """Dispatch container_post api call"""
216 request_json = {}
217 if data_json['post_action'] == 'exec':
218 if 'request' in data_json:
219 request_json = data_json['request']
220 if 'cmd' in request_json:
221 if 'task' in request_json:
222 api_call_method_name = '__'.join(['container_post', str(data_json['post_action']), str(request_json['cmd']), str(request_json['task']) ])
223 else:
224 dockerapi.logger.error("api call: task missing")
225 else:
226 dockerapi.logger.error("api call: cmd missing")
227 else:
228 dockerapi.logger.error("api call: request missing")
229 else:
230 api_call_method_name = '__'.join(['container_post', str(data_json['post_action'])])
231
232 if api_call_method_name:
233 api_call_method = getattr(dockerapi, api_call_method_name)
234 if api_call_method:
235 dockerapi.logger.info("api call: %s, container_name: %s" % (api_call_method_name, data_json['container_name']))
236 api_call_method(request_json, container_name=data_json['container_name'])
237 else:
238 dockerapi.logger.error("api call not found: %s, container_name: %s" % (api_call_method_name, data_json['container_name']))
239 except Exception as e:
240 dockerapi.logger.error("container_post: %s" % str(e))
241 else:
242 dockerapi.logger.error("api call: missing container_name, post_action or request")
243 else:
244 dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
245 else:
246 dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
247
248 await asyncio.sleep(0.0)
249 except asyncio.TimeoutError:
250 pass
251
252if __name__ == '__main__':
253 uvicorn.run(
254 app,
255 host="0.0.0.0",
256 port=443,
257 ssl_certfile="/app/dockerapi_cert.pem",
258 ssl_keyfile="/app/dockerapi_key.pem",
259 log_level="info",
260 loop="none"
261 )