Files
bin/node-collector-last-docker-update.py

179 lines
6.5 KiB
Python
Executable File

#!/usr/bin/python3
import yaml
import subprocess
import re
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
# these ports are nat'd on the modem to mara, so if a container is in host mode
# on one of these ports, or maps these potrs, then that container is internet-facing
PORT_FWDS=[ '7777', '7778', '27015', '993', '25', '465', '587' ]
# see if this container either has a matching port to the list of open ports on the modem (PORT_FWDS)
# or if traefik is front-ending this, and the rule includes a Host that has depaoli.id.au on it
# if the above are true, its internet-facing, and a higher risk, so worth knowing
def external(container):
if 'network_mode' in compose['services'][container]:
return "Maybe"
if 'ports' in compose['services'][container]:
for port in PORT_FWDS:
if any(port in substring for substring in compose['services'][container]['ports']):
return True
if 'labels' not in compose['services'][container]:
return False
traefik_on=False
ext_host=False
for s in compose['services'][container]['labels']:
if 'traefik.enable=true' in s:
if 'true' in s:
traefik_on=True
m=re.search( r"traefik.\S+.routers.\S+.rule=\s*Host\S+\(\`(.*)\`\)", s )
if m and 'depaoli.id.au' in m[1]:
ext_host=True
if traefik_on and ext_host:
return True
else:
return False
def watchtower(container):
if 'labels' not in compose['services'][container]:
return False
for s in compose['services'][container]['labels']:
if 'watchtower' in s:
if 'true' in s:
return True
return False
# take a dt_str like 2020-12-10T09:21:12+11:00 and convert into a datetime
# then work out how many hours extra the timezone (will be 10 or 11)
# then remove the +TZ, and then add it as actual hours
# this seems mad that there is no func for this, but it works
def norm_time(dt_str):
d=datetime.strptime(dt_str, '%Y-%m-%dT%H:%M:%S%z')
return d.replace(tzinfo=None)
def norm_time_git(dt_str):
d=datetime.strptime(dt_str, '%Y-%m-%d %H:%M:%S %z')
return d.replace(tzinfo=None)
# Load the docker-compose.yaml file
with open('/srv/docker/config/docker-compose.yml') as f:
compose = yaml.safe_load(f)
# Get the container names from the docker-compose.yaml file
containers = [service_name for service_name in compose['services'].keys()]
# Load the update-docker.log file
with open('/srv/docker/log/update-docker.log') as f:
logs = f.readlines()
# Find the last log line for each container
last_logs = {}
for container in containers:
last_log = None
for log in logs:
if 'image' in compose['services'][container]:
im=compose['services'][container]['image']
else:
im=''
if im != "" and im in log:
last_log = log
last_logs[container] = last_log
current_datetime = datetime.now()
# open file for writing prometheus formatted data into
f = open('/srv/docker/container/node-exporter/textfile_collector/docker_updates.prom', 'w')
# put required help/type text in
print('# HELP node_docker_updates details of last known update of a container, whether it has a locked version (or latest tag) and whether watchtower is updating it', file=f)
print('# TYPE node_docker_updates gauge', file=f )
# Print the last log line and container name for each container
for container, last_log in last_logs.items():
out_str= 'node_docker_updates{container="'
out_str += container
out_str += '", watchtower='
if watchtower( container ):
out_str += '"yes"'
else:
out_str += '"no"'
if 'image' in compose['services'][container]:
im=compose['services'][container]['image']
else:
im=''
out_str +=', latest_tag='
if ':' in im and ':latest' not in im and ':nightly' not in im:
out_str += '"no"'
else:
out_str += '"yes"'
is_built=0
out_str += ', image="'
if 'image' in compose['services'][container]:
out_str += compose['services'][container]['image']
else:
out_str += 'Built'
is_built=1
out_str += '"'
if last_log:
dt_str=last_log.split()[0].split('=')[1].replace('"','')
last_update=f'{norm_time(dt_str)}'
else:
if 'image' in compose['services'][container]:
res=subprocess.run(['sudo','docker','image','history','--human=false',compose['services'][container]['image']], stdout=subprocess.PIPE)
if res.returncode == 0:
m=re.search(r'(\d{1,4}-\d{1,2}-\d{1,2}T\d{1,2}:\d{1,2}:\d{1,2}\+\d{1,2}:\d{1,2})', str(res.stdout, 'utf-8') )
if '0001-01-01' in m[1]:
m=re.findall( r'(\d{1,4}-\d{1,2}-\d{1,2}T\d{1,2}:\d{1,2}:\d{1,2})', str(res.stdout, 'utf-8') )
cdate=norm_time( m[1] + '+10:00')
else:
cdate=norm_time( m[1] )
last_update=cdate
else:
last_update='No Date'
else:
res=subprocess.run(['sudo','docker','history','--human=false',f'config_{container}'], stdout=subprocess.PIPE)
if res.returncode == 0:
m=re.search(r'(\d{1,4}-\d{1,2}-\d{1,2}T\d{1,2}:\d{1,2}:\d{1,2}\+\d{1,2}:\d{1,2})', str(res.stdout, 'utf-8') )
cdate=norm_time( m[1] )
last_update=cdate
else:
last_update='No Date'
# if is_built and 'book' in container:
# res=subprocess.run(['git', '-C', '/home/ddp/src/pybook/', 'log', '--date=iso', '-n', '1', '--pretty=%ci'], stdout=subprocess.PIPE)
# last_update=norm_time_git( str(res.stdout, 'utf-8').strip() )
#
# if is_built and 'pa' in container:
# res=subprocess.run(['git', '-C', '/home/ddp/src/photoassistant/', 'log', '--date=iso', '-n', '1', '--pretty=%ci'], stdout=subprocess.PIPE)
# last_update=norm_time_git( str(res.stdout, 'utf-8').strip() )
if last_update == 'No Date':
out_str += f', age_in_days="99999"'
else:
if type(last_update) == type(current_datetime):
last_update_as_datetime = last_update
else:
last_update_as_datetime = datetime.strptime( last_update, '%Y-%m-%d %H:%M:%S' )
time_difference = current_datetime - last_update_as_datetime
out_str += f', age_in_days="{time_difference.days}"'
out_str += f', last_update="{last_update}"'
out_str += f', internet_facing="{external(container)}"'
out_str += '} 1'
print( out_str, file=f )
f.close()