Skip to content

Commit a6b9f66

Browse files
authored
Merge pull request #10 from m-erhardt/refactor-daemon-api-151
### Changes - **BREAKING** Change `check_container_stats_docker.py ` to use Docker engine API `v1.51` as support for engine API `v1.46` will most likely be dropped at some point. Docker engine API `v1.51` is available in Docker versions from `28.3` upwards. - Fix socket bug when running plugin on MacOS - Update GitHub action versions - Update Python versions in CI - Run unit tests with all supported Python versions - Run linters only with newest Python version
2 parents 970e105 + 2fc929a commit a6b9f66

File tree

5 files changed

+96
-84
lines changed

5 files changed

+96
-84
lines changed

.github/workflows/pycodestyle.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ jobs:
1414
runs-on: ubuntu-latest
1515
strategy:
1616
matrix:
17-
python-version: ["3.9", "3.10", "3.11"]
17+
python-version: ["3.14"]
1818
steps:
1919
- name: Check out repository
20-
uses: actions/checkout@v4
20+
uses: actions/checkout@v5
2121

2222
- name: Set up Python ${{ matrix.python-version }}
23-
uses: actions/setup-python@v5
23+
uses: actions/setup-python@v6
2424
with:
2525
python-version: ${{ matrix.python-version }}
2626

.github/workflows/pylint.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ jobs:
1414
runs-on: ubuntu-latest
1515
strategy:
1616
matrix:
17-
python-version: ["3.9", "3.10", "3.11"]
17+
python-version: ["3.14"]
1818
steps:
1919
- name: Check out repository
20-
uses: actions/checkout@v4
20+
uses: actions/checkout@v5
2121

2222
- name: Set up Python ${{ matrix.python-version }}
23-
uses: actions/setup-python@v5
23+
uses: actions/setup-python@v6
2424
with:
2525
python-version: ${{ matrix.python-version }}
2626

.github/workflows/python_unittests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ jobs:
1414
runs-on: ubuntu-latest
1515
strategy:
1616
matrix:
17-
python-version: ["3.9", "3.10", "3.11"]
17+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
1818
steps:
1919
- name: Check out repository
20-
uses: actions/checkout@v4
20+
uses: actions/checkout@v5
2121

2222
- name: Set up Python ${{ matrix.python-version }}
23-
uses: actions/setup-python@v5
23+
uses: actions/setup-python@v6
2424
with:
2525
python-version: ${{ matrix.python-version }}
2626

check_container_stats_docker.py

Lines changed: 51 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ def send_http_get(
8989
) -> dict:
9090
""" Prepare HTTP post reqest to be sent to docker socket, evluate HTTP response """
9191

92-
cmd: str = (f'GET { endpoint } HTTP/1.1\r\n'
93-
f'Host: { host }\r\n'
94-
f'User-Agent: { useragent }\r\n'
92+
cmd: str = (f'GET {endpoint} HTTP/1.1\r\n'
93+
f'Host: {host}\r\n'
94+
f'User-Agent: {useragent}\r\n'
9595
f'Accept: application/json\r\n'
9696
f'Connection: close\r\n\r\n')
9797

@@ -122,9 +122,9 @@ def send_http_get(
122122
response["http_response"]: dict = json.loads(buf_lines[-1])
123123

124124
if response["http_status"] != 200:
125-
exit_plugin(2, (f'Daemon API v{ response["http_header_api-version"] } returned HTTP '
126-
f'{ response["http_status"] } while fetching { endpoint }: '
127-
f'{ response["http_response"] }'), '')
125+
exit_plugin(2, (f'Daemon API v{response["http_header_api-version"]} returned HTTP '
126+
f'{response["http_status"]} while fetching {endpoint}: '
127+
f'{response["http_response"]}'), '')
128128

129129
return response
130130

@@ -167,19 +167,25 @@ def send_socket_cmd(cmd: str, socketfile: str) -> str:
167167
buf += data.decode()
168168

169169
# Shut down sending
170-
sock.shutdown(socket.SHUT_WR)
170+
try:
171+
sock.shutdown(socket.SHUT_WR)
172+
except OSError:
173+
# catch "OSError: [Errno 57] Socket is not connected" on MacOS
174+
# Apparently Docker on MacOS shuts down the socket connection at this point already due to our
175+
# "Connection: close" header
176+
pass
171177

172178
# Close socket connection
173179
sock.close()
174180

175181
except FileNotFoundError:
176-
exit_plugin(3, f'Socket file { socketfile } not found!', "")
182+
exit_plugin(3, f'Socket file {socketfile} not found!', "")
177183
except PermissionError:
178-
exit_plugin(3, f'Access to socket file { socketfile } denied!', "")
184+
exit_plugin(3, f'Access to socket file {socketfile} denied!', "")
179185
except (TimeoutError, socket.timeout):
180-
exit_plugin(3, f'Connection to socket { socketfile } timed out!', "")
186+
exit_plugin(3, f'Connection to socket {socketfile} timed out!', "")
181187
except ConnectionError as err:
182-
exit_plugin(3, f'Error during socket connection: { err }', "")
188+
exit_plugin(3, f'Error during socket connection: {err}', "")
183189

184190
return buf
185191

@@ -238,15 +244,15 @@ def set_state(newstate: int, state: int) -> int:
238244
def convert_bytes_to_pretty(raw_bytes: int) -> str:
239245
""" converts raw bytes into human readable output """
240246
if raw_bytes >= 1099511627776:
241-
output = f'{ round(raw_bytes / 1024 **4, 2) }TiB'
247+
output = f'{round(raw_bytes / 1024 ** 4, 2)}TiB'
242248
elif raw_bytes >= 1073741824:
243-
output = f'{ round(raw_bytes / 1024 **3, 2) }GiB'
249+
output = f'{round(raw_bytes / 1024 ** 3, 2)}GiB'
244250
elif raw_bytes >= 1048576:
245-
output = f'{ round(raw_bytes / 1024 **2, 2) }MiB'
251+
output = f'{round(raw_bytes / 1024 ** 2, 2)}MiB'
246252
elif raw_bytes >= 1024:
247-
output = f'{ round(raw_bytes / 1024, 2) }KiB'
253+
output = f'{round(raw_bytes / 1024, 2)}KiB'
248254
elif raw_bytes < 1024:
249-
output = f'{ raw_bytes }B'
255+
output = f'{raw_bytes}B'
250256
else:
251257
# Theoretically impossible, prevent pylint possibly-used-before-assignment
252258
raise ValueError('Impossible value in convert_bytes_to_pretty()')
@@ -258,25 +264,25 @@ def get_container_from_name(args: Arguments) -> dict:
258264

259265
# Query all containers that match the given name from /containers/json
260266
containers: dict = send_http_get(
261-
f'/v1.45/containers/json?all=true&filters={{"name":["{ args.container_name }"]}}',
267+
f'/v1.51/containers/json?all=true&filters={{"name":["{args.container_name}"]}}',
262268
socketfile=args.socket
263269
)
264270

265271
if len(containers["http_response"]) == 0:
266-
exit_plugin(2, f'No container matched name { args.container_name }', '')
272+
exit_plugin(2, f'No container matched name {args.container_name}', '')
267273
elif args.wildcard is True and len(containers["http_response"]) > 1:
268-
exit_plugin(2, f'Multiple containers matched wildcard name { args.container_name }', '')
274+
exit_plugin(2, f'Multiple containers matched wildcard name {args.container_name}', '')
269275

270276
if args.wildcard is True:
271277
# We previously checked than wildcard name matched only one container - so this must be it
272278
container_info: dict = containers["http_response"][0]
273279
else:
274280
# loop over returned containers and check for match
275281
for cnt in containers["http_response"]:
276-
if f'/{ args.container_name }' in cnt["Names"]:
282+
if f'/{args.container_name}' in cnt["Names"]:
277283
container_info: dict = cnt
278284
if 'container_info' not in locals():
279-
exit_plugin(2, f'No container matched name { args.container_name }', '')
285+
exit_plugin(2, f'No container matched name {args.container_name}', '')
280286

281287
return container_info
282288

@@ -289,13 +295,13 @@ def calc_container_metrics(info: dict, stats: dict) -> dict:
289295

290296
try:
291297
# Extract container name and id from api response
292-
container.update({"name": f'{ info["Names"][0][1:] }'})
293-
container.update({"id": f'{ info["Id"][:12] }'})
294-
container.update({"id_long": f'{ info["Id"] }'})
298+
container.update({"name": f'{info["Names"][0][1:]}'})
299+
container.update({"id": f'{info["Id"][:12]}'})
300+
container.update({"id_long": f'{info["Id"]}'})
295301

296302
# Get container state
297-
container.update({"state": f'{ info["State"] }'})
298-
container.update({"status": f'{ info["Status"] }'})
303+
container.update({"state": f'{info["State"]}'})
304+
container.update({"status": f'{info["Status"]}'})
299305

300306
# Get process statistics
301307
container.update({"pid_count": stats["pids_stats"].get("current", 0)})
@@ -353,7 +359,7 @@ def calc_container_metrics(info: dict, stats: dict) -> dict:
353359
container.update({"blk_io": {"r": blkio_r, "w": blkio_w}})
354360

355361
except KeyError as err:
356-
exit_plugin(2, f'Error while extracting values from JSON response: { err }', "")
362+
exit_plugin(2, f'Error while extracting values from JSON response: {err}', "")
357363

358364
return container
359365

@@ -367,45 +373,45 @@ def main():
367373
# Get daemon API version
368374
server_version: dict = send_http_get('/version', socketfile=args.socket)["http_response"]
369375

370-
# Check of daemon is compatible with API v1.45
371-
if tuple(server_version["MinAPIVersion"].split('.')) > ("1", "45"):
372-
exit_plugin(2, (f'This plugin requires a docker daemon supporting API version 1.45 - '
376+
# Check of daemon is compatible with API v1.51
377+
if tuple(server_version["MinAPIVersion"].split('.')) >= ("1", "51"):
378+
exit_plugin(2, (f'This plugin requires a docker daemon supporting API version 1.51 - '
373379
f'Minimum supported version of this docker daemon is '
374-
f'{ server_version["MinAPIVersion"] }'), '')
380+
f'{server_version["MinAPIVersion"]}'), '')
375381

376382
# Get container id for name from /containers/json
377383
# Returns Container JSON object as dict
378384
container_info: dict = get_container_from_name(args)
379385

380386
# Check if container is running, if not we can exit early without perfdata
381387
if container_info["State"] != "running":
382-
exit_plugin(2, f'Container { container_info["Names"][0][1:] } is { container_info["Status"] }', '')
388+
exit_plugin(2, f'Container {container_info["Names"][0][1:]} is {container_info["Status"]}', '')
383389

384390
# Get container stats
385391
container_stats: dict = send_http_get(
386-
f'/v1.45/containers/{ container_info["Id"] }/stats?stream=false&one-shot=false',
392+
f'/v1.51/containers/{container_info["Id"]}/stats?stream=false&one-shot=false',
387393
socketfile=args.socket
388394
)["http_response"]
389395

390396
# Hand raw API responses over to calc_container_metrics to extract attributes and derive metrics
391397
container: dict = calc_container_metrics(container_info, container_stats)
392398

393399
# Construct perfdata and output
394-
output = (f"{ container['name'] } ({ container['id'] }) is { container['status'] } - "
395-
f"CPU: { container['cpu_pct'] }%, "
396-
f"Memory: { convert_bytes_to_pretty(container['memory']['used']) }, "
400+
output = (f"{container['name']} ({container['id']}) is {container['status']} - "
401+
f"CPU: {container['cpu_pct']}%, "
402+
f"Memory: {convert_bytes_to_pretty(container['memory']['used'])}, "
397403
f"PIDs: {container['pid_count']}")
398404

399-
perfdata = (f" | cpu={ container['cpu_pct'] }%;{ args.cpuwarn or '' };"
400-
f"{ args.cpucrit or '' };; "
401-
f"pids={ container['pid_count'] };{ args.pidwarn or '' };"
402-
f"{ args.pidcrit or '' };0;{ container['pid_limit'] } "
405+
perfdata = (f" | cpu={container['cpu_pct']}%;{args.cpuwarn or ''};"
406+
f"{args.cpucrit or ''};; "
407+
f"pids={container['pid_count']};{args.pidwarn or ''};"
408+
f"{args.pidcrit or ''};0;{container['pid_limit']} "
403409
f"mem={container['memory']['used']}B;{args.memwarn or ''};"
404-
f"{args.memcrit or ''};0;{ container['memory']['available'] } "
405-
f"net_send={ container['net_io']['tx'] }B;;;; "
406-
f"net_recv={ container['net_io']['rx'] }B;;;; "
407-
f"disk_read={ container['blk_io']['r'] }B;;;; "
408-
f"disk_write={ container['blk_io']['w'] }B;;;; ")
410+
f"{args.memcrit or ''};0;{container['memory']['available']} "
411+
f"net_send={container['net_io']['tx']}B;;;; "
412+
f"net_recv={container['net_io']['rx']}B;;;; "
413+
f"disk_read={container['blk_io']['r']}B;;;; "
414+
f"disk_write={container['blk_io']['w']}B;;;; ")
409415

410416
# Set initial return code
411417
returncode = 0

check_docker_system.py

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -93,19 +93,25 @@ def send_socket_cmd(cmd: str, socketfile: str) -> str:
9393
buf += data.decode()
9494

9595
# Shut down sending
96-
sock.shutdown(socket.SHUT_WR)
96+
try:
97+
sock.shutdown(socket.SHUT_WR)
98+
except OSError:
99+
# catch "OSError: [Errno 57] Socket is not connected" on MacOS
100+
# Apparently Docker on MacOS shuts down the socket connection at this point already due to our
101+
# "Connection: close" header
102+
pass
97103

98104
# Close socket connection
99105
sock.close()
100106

101107
except FileNotFoundError:
102-
exit_plugin(3, f'Socket file { socketfile } not found!', "")
108+
exit_plugin(3, f'Socket file {socketfile} not found!', "")
103109
except PermissionError:
104-
exit_plugin(3, f'Access to socket file { socketfile } denied!', "")
110+
exit_plugin(3, f'Access to socket file {socketfile} denied!', "")
105111
except (TimeoutError, socket.timeout):
106-
exit_plugin(3, f'Connection to socket { socketfile } timed out!', "")
112+
exit_plugin(3, f'Connection to socket {socketfile} timed out!', "")
107113
except ConnectionError as err:
108-
exit_plugin(3, f'Error during socket connection: { err }', "")
114+
exit_plugin(3, f'Error during socket connection: {err}', "")
109115

110116
return buf
111117

@@ -150,9 +156,9 @@ async def send_http_get(
150156
host: str = 'localhost', useragent: str = 'check_docker_system.py') -> dict:
151157
""" Prepare HTTP post reqest to be sent to docker socket """
152158

153-
cmd: str = (f'GET { endpoint } HTTP/1.1\r\n'
154-
f'Host: { host }\r\n'
155-
f'User-Agent: { useragent }\r\n'
159+
cmd: str = (f'GET {endpoint} HTTP/1.1\r\n'
160+
f'Host: {host}\r\n'
161+
f'User-Agent: {useragent}\r\n'
156162
f'Accept: application/json\r\n'
157163
f'Connection: close\r\n\r\n')
158164

@@ -222,15 +228,15 @@ def parse_docker_sysinfo(docker_sysinfo: dict) -> dict:
222228
def convert_bytes_to_pretty(raw_bytes: int):
223229
""" converts raw bytes into human readable output """
224230
if raw_bytes >= 1099511627776:
225-
output = f'{ round(raw_bytes / 1024 **4, 2) }TiB'
231+
output = f'{round(raw_bytes / 1024 ** 4, 2)}TiB'
226232
elif raw_bytes >= 1073741824:
227-
output = f'{ round(raw_bytes / 1024 **3, 2) }GiB'
233+
output = f'{round(raw_bytes / 1024 ** 3, 2)}GiB'
228234
elif raw_bytes >= 1048576:
229-
output = f'{ round(raw_bytes / 1024 **2, 2) }MiB'
235+
output = f'{round(raw_bytes / 1024 ** 2, 2)}MiB'
230236
elif raw_bytes >= 1024:
231-
output = f'{ round(raw_bytes / 1024, 2) }KiB'
237+
output = f'{round(raw_bytes / 1024, 2)}KiB'
232238
elif raw_bytes < 1024:
233-
output = f'{ raw_bytes }B'
239+
output = f'{raw_bytes}B'
234240
else:
235241
# Theoretically impossible, prevent pylint possibly-used-before-assignment
236242
raise ValueError('Impossible value in convert_bytes_to_pretty()')
@@ -283,9 +289,9 @@ def main():
283289

284290
# Check HTTP response code
285291
if state["http_status"] not in [200]:
286-
exit_plugin(3, f'Docker socket returned HTTP { state["http_status"] }: { state["http_response"] }', '')
292+
exit_plugin(3, f'Docker socket returned HTTP {state["http_status"]}: {state["http_response"]}', '')
287293
elif volumes["http_status"] not in [200]:
288-
exit_plugin(3, f'Docker socket returned HTTP { volumes["http_status"] }: { volumes["http_response"] }', '')
294+
exit_plugin(3, f'Docker socket returned HTTP {volumes["http_status"]}: {volumes["http_response"]}', '')
289295

290296
if args.debug is True:
291297
print(json.dumps(state, indent=4))
@@ -296,7 +302,7 @@ def main():
296302
docker_sysinfo: dict = json.loads(state["http_response"])
297303
docker_volinfo: dict = json.loads(volumes["http_response"])
298304
except json.decoder.JSONDecodeError as err:
299-
exit_plugin(3, f'Unable to parse valid JSON from docker daemon response: { err }', '')
305+
exit_plugin(3, f'Unable to parse valid JSON from docker daemon response: {err}', '')
300306

301307
engine_state = parse_docker_sysinfo(docker_sysinfo)
302308
volcount: int = len(docker_volinfo["Volumes"])
@@ -316,22 +322,22 @@ def main():
316322
state = set_state(1, state)
317323

318324
output = (
319-
f'Containers: { engine_state["containers"]["total"] } '
320-
f'(Running: { engine_state["containers"]["running"] }, Paused: { engine_state["containers"]["paused"] }, '
321-
f'Stopped: { engine_state["containers"]["stopped"] }), Images: { engine_state["images"] }, '
322-
f'Volumes: { volcount }, '
323-
f'Docker version { engine_state["server_version"] } running with '
324-
f'{ engine_state["cpus"] } CPUs and { convert_bytes_to_pretty(engine_state["memory"]) } memory'
325+
f'Containers: {engine_state["containers"]["total"]} '
326+
f'(Running: {engine_state["containers"]["running"]}, Paused: {engine_state["containers"]["paused"]}, '
327+
f'Stopped: {engine_state["containers"]["stopped"]}), Images: {engine_state["images"]}, '
328+
f'Volumes: {volcount}, '
329+
f'Docker version {engine_state["server_version"]} running with '
330+
f'{engine_state["cpus"]} CPUs and {convert_bytes_to_pretty(engine_state["memory"])} memory'
325331
)
326332
perfdata = (
327-
f'\'containers_running\'={ engine_state["containers"]["running"] };;;0;'
328-
f'{ engine_state["containers"]["total"] } '
329-
f'\'containers_paused\'={ engine_state["containers"]["paused"] };{ args.maxpaused or "" };;0;'
330-
f'{ engine_state["containers"]["total"] } '
331-
f'\'containers_stopped\'={ engine_state["containers"]["stopped"] };{ args.maxstopped or "" };;0;'
332-
f'{ engine_state["containers"]["total"] } '
333-
f'\'images\'={ engine_state["images"] };{ args.maximages or "" };;0; '
334-
f'\'volumes\'={ volcount };{ args.maxvolumes or "" };;0;'
333+
f'\'containers_running\'={engine_state["containers"]["running"]};;;0;'
334+
f'{engine_state["containers"]["total"]} '
335+
f'\'containers_paused\'={engine_state["containers"]["paused"]};{args.maxpaused or ""};;0;'
336+
f'{engine_state["containers"]["total"]} '
337+
f'\'containers_stopped\'={engine_state["containers"]["stopped"]};{args.maxstopped or ""};;0;'
338+
f'{engine_state["containers"]["total"]} '
339+
f'\'images\'={engine_state["images"]};{args.maximages or ""};;0; '
340+
f'\'volumes\'={volcount};{args.maxvolumes or ""};;0;'
335341
)
336342

337343
exit_plugin(state, output, perfdata)

0 commit comments

Comments
 (0)