Skip to content
Snippets Groups Projects
Verified Commit ef5a6e85 authored by Quentin Duchemin's avatar Quentin Duchemin
Browse files

Remove unused folder from merge

parent 6ed2b29d
No related branches found
No related tags found
1 merge request!53Migrate to Traefik v2
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don’t work, or not
# install all needed dependencies.
#Pipfile.lock
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
FROM alpine:3.9
WORKDIR /usr/src/app
COPY requirements.txt ./
RUN apk add --no-cache python3 && \
python3 -m ensurepip && \
rm -r /usr/lib/python*/ensurepip && \
pip3 install --upgrade pip setuptools -r requirements.txt && \
rm -r /root/.cache
# ACME Copy certs
Ce service permet l'exploitation des certificats TLS générés par Traefik
par d'autres serveurs pour leur propres besoins de chiffrement de connexions.
Traefik a la capacité d'automatiser la demande et le renouvellement de certificats
TLS auprès de services en ligne tels que [Let's Encrypt](https://letsencrypt.org)
pour assurer le chiffrement des connexions HTTPS aux sites web dont il a la charge.
L'idée est de déléguer la gestion de certificats utilisés par d'autres services dans
d'autres conteneurs à Traefik, ce qui présente les avantages suivants :
* Les certificats générés sont signés par une autorité de certification publique dont les
certificats sont déjà présents sur la plupart des systèmes, ce qui facilite la
vérification de l'authenticité des certificats par rapport à des certificats auto-signés (pas
de certificats de signature à déployer)
* A leur expiration, les certificats sont automatiquement renouvelés, ce qui élimine au tâche d'administration
et garantit la disponibilité continue des services
\ No newline at end of file
""" Traefik Certificate extraction and update module
Provides tools to extract TLS certificates from Treafik acme.json files.
Classes
-------
CertUpdater:
This class handles acme files decoding and stores or updates the resulting
certificates in separate folders.
"""
import os
import errno
import json
from base64 import b64decode
class CertUpdater:
"""Decodes acme files and extact the resulting certificates if needed.
This class keeps flat certificates for specified domain names
in sync with the contents of an acme.json file. The certificates are stored in separated
subdirectories named after the main domain name they are valid for.
If the certificates specify x509 SAN records, only the main CN is used (a single
directory is created, regardless of the number of alternate names.)
Attributes
----------
acme_file : str
Name of the acme.json file containing the certificates
names : list
List of domain names to keep updated
certs_root_path : str
Directory where the certificates will be stored. The certificates are
stored in subdirectories of this directory
Methods
-------
add(name)
Adds a domain name to the updater
remove(name)
Removes a domain name from the updater
update()
Decodes the acme.json file and updates the certicates that need to be updated.
"""
def __init__(self, acme_file, certs_root_path):
"""
Parameters
----------
acme_file : str
path of the acme file containing the certificates
certs_root_path : str
root directory where certificates will be extracted.
They are extracted in subdirectories named after the CN stored in
the certificate.
"""
self.acme_file = acme_file
self.names = []
self.certs_root_path = certs_root_path + '/'
def add(self, name):
"""Adds a domain name to the updater.
Parameters
----------
name : str
domain name to add
"""
self.names.append(name)
# Create directories etc if they do not exist
try:
os.makedirs(self.certs_root_path + name + '/')
except OSError as error:
if error.errno != errno.EEXIST:
raise
def remove(self, name):
"""Removes a domain name from the updater.
Parameters
----------
name : str
domain name to remove
"""
try:
self.names.remove(name)
except ValueError:
pass
def update(self):
"""Decodes the acme.json file and updates the certicates that need to be updated.
Returns
-------
list
a list of domain names that have been updated. An empty list if no certificate
has been updated.
"""
# Open and decode the acme file
try:
with open(self.acme_file) as acme:
data = json.loads(acme.read())
except FileNotFoundError:
print('ACME file {0} not found when trying to decode.'.format(self.acme_file))
return []
except json.JSONDecodeError:
print('File {0} does not look like an ACME file.'.format(self.acme_file))
return []
# Get the acme file version
try:
acme_ver = 2 if 'acme-v02' in data['Account']['Registration']['uri'] else 1
except TypeError:
if 'DomainsCertificate' in data:
acme_ver = 1
else:
acme_ver = 2
# Get the certificates
if acme_ver == 1:
certs = data['DomainsCertificate']['Certs']
elif acme_ver == 2:
certs = data['Certificates']
# Iterate over certificates. If a certificate has been updated,
# add its name to the updated_names.
updated_names = []
for c in certs:
if acme_ver == 1:
name = c['Certificate']['Domain']
key = c['Certificate']['PrivateKey']
fullchain = c['Certificate']['Certificate']
elif acme_ver == 2:
name = c['Domain']['Main']
key = c['Key']
fullchain = c['Certificate']
if name in self.names:
key = b64decode(key).decode('utf-8')
fullchain = b64decode(fullchain).decode('utf-8')
chain_start = fullchain.find('-----BEGIN CERTIFICATE-----', 1)
cert = fullchain[0:chain_start]
chain = fullchain[chain_start:]
print('Updating certificates for {0}'.format(name))
if self._needs_updating(name, fullchain):
path = self.certs_root_path + name + '/'
with open(path + 'privkey.pem', 'w') as f:
f.write(key)
with open(path + 'cert.pem', 'w') as f:
f.write(cert)
with open(path + 'chain.pem', 'w') as f:
f.write(chain)
with open(path + 'fullchain.pem', 'w') as f:
f.write(fullchain)
print('Certificates updated')
updated_names.append(name)
else:
print('Cetificates are already up-to-date')
return updated_names
def _needs_updating(self, name, fullchain):
"""Checks if a certificate has changed
Parameters
----------
name : str
Name of the directory containing the certificates
fullchain : str
Full certificates extracted from the acme.json file
Returns
-------
bool
True if the contents of the current certificates are
different from the fullchain. False otherwise.
"""
path = self.certs_root_path + name + '/fullchain.pem'
try:
with open(path, 'r') as f:
return f.read() != fullchain
except FileNotFoundError:
return True
\ No newline at end of file
import docker
class DockerAction:
def __init__(self, docker_client, container_id):
self.client = docker_client
self.container_id = container_id
pass
def exec(self, id):
c = self.client.container.get(self.container_id)
if c.status == 'running':
self.action(c)
def action(self, container):
pass
class RestartDockerAction(DockerAction):
def __init__(self, docker_client, container_id):
super().__init__(docker_client, container_id)
def action(self, container):
try:
container.restart()
except docker.errors.APIError:
print('Unable to restart container {0}({1})'.format(container.name,
container.id))
class KillDockerAction(DockerAction):
def __init__(self, docker_client, container_id, signal='SIGKILL'):
super().__init__(docker_client, container_id)
self.signal = signal
def action(self, container):
try:
container.kill(self.signal)
except docker.errors.APIError:
print('Unable to kill container {0}({1}) with signal {2}'
.format(container.name, self.container_id, self.signal))
\ No newline at end of file
"""Docker event monitoring
Provides a monitor for docker events that add/remove services when
they occur according to the configuration provided in the labels of these
services.
Classes
-------
DockerMonitor
A monitor for docker events.
"""
import docker
import threading
import requests
from service_manager import Service
from docker_actions import RestartDockerAction
from docker_actions import KillDockerAction
class DockerMonitor(threading.Thread):
"""Implements a docker events monitoring thread.
This class implements a thread that listen to the docker socket to intercept
events on specific containers to launch actions on these when start and stop events occur.
Containers that should be monitored are identified by labels.
"""
def __init__(self, services_mgr):
self.docker_client = docker.from_env()
self.events = None
self.services_mgr = services_mgr
super().__init__()
def run(self):
print('Starting monitor')
# Identify all running containers and get dependent services
# at startup
try:
containers = self.docker_client.containers.list()
for c in containers:
if c.status == 'running':
self._add_service(c)
# Then, wait for docker events and identify new dependent services
# or services that exit the system. Do an update each time a new
# service is added / restarted etc. or a service is changed.
self.events = self.docker_client.events(decode = True)
for event in self.events:
if 'status' not in event:
continue
try:
c = self.docker_client.containers.get(event['id'])
if event['status'] == 'stop':
self._remove_service(c)
elif event['status'] == 'start':
self._add_service(c)
except docker.errors.NotFound:
pass
except docker.errors.APIError as error:
print('Docker error while looking up container {0}: {1} '.format(event.id, error.strerror))
except requests.exceptions.ConnectionError as error:
print('Connection to docker socket refused.')
print('No more events to handle: stopping monitor')
def stop(self):
if self.events is not None:
self.events.close()
self.join()
def _get_host_from_traefik_rule(self, container):
if 'traefik.frontend.rule' in container.labels:
try:
return container.labels['traefik.frontend.rule'].split('Host:')[1].split(',')[0].strip()
except IndexError:
return ''
def _get_action_from_label(self, container):
if 'acme_copy_certs.action' not in container.labels:
return None
try:
action = container.label('acme_copy_certs.action')
if action[0].strip() == 'kill':
if len(action) == 1:
return KillDockerAction(self.docker_client, container.id, 'SIGHUP')
else:
return KillDockerAction(self.docker_client, container.id, action[1].strip())
elif action[0].strip() == 'restart':
return RestartDockerAction(self.docker_client, container.id)
else:
return None
except IndexError:
print('Invalid action')
return None
def _add_service(self, container):
if 'acme_copy_certs.enable' in container.labels:
if container.labels['acme_copy_certs.enable'] == 'true':
host = self._get_host_from_traefik_rule(container)
elif container.labels['acme_copy_certs.enable'] == 'false':
pass
else:
print('Invalid acme_copy_certs.enable value for {0}'
.format(container.name))
if host:
print('Handling container {0}({1})'.format(container.name, container.id))
s = Service(container.id, host, self._get_action_from_label(container))
self.services_mgr.add(s)
else:
print('Not handling container {0}({1})'.format(container.name, container.id))
def _remove_service(self, container):
self.services_mgr.remove(container.id)
\ No newline at end of file
import os
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler
class FileWatcher:
def __init__(self, file_path, handler):
self.observer = Observer()
abs_path, file_name = os.path.split(os.path.abspath(file_path))
self.observed_path = abs_path
self.observed_file = file_name
self.handler = handler
self.event_handler = PatternMatchingEventHandler(
patterns=['*/' + file_name],
ignore_patterns=[],
ignore_directories=True)
self.event_handler.on_any_event = self._on_any_event
self.observer.schedule(self.event_handler,
self.observed_path,
recursive=False)
def _on_any_event(self, event):
self.handler()
def start(self):
print('Starting observing file {0} in directory {1}'.format(self.observed_file, self.observed_path))
self.observer.start()
def stop(self):
self.observer.stop()
self.observer.join()
\ No newline at end of file
"""Handling of docker services
This module provides a ServicesManager class implementing a collection of
services whose certificates are to be updated by the ssytem.
Classes
-------
Service
A class representing a single service
ServiceManager
A class handling a collection of services
"""
from threading import Lock
class Service:
"""
A service wrapper This object represents a docker service
Attributes
----------
id : int
The id of the container running the service
host : str
The hostname associated with the service used for CN in TLS certs
action : DockerAction
An action to perform on the service when the run_action method is
invoked.
Methods
-------
run_action()
Runs the action bound to this service
"""
def __init__(self, id, host, action):
"""
Parameters
----------
id : int
The id of the container running the service
host : str
The hostname associated with the service used for CN in TLS certs
action : DockerAction
An action to perform on the service when the run_action method is invoked.
"""
self.id = id
self.host = host
self.action = action
def run_action(self):
"""Executes the action on the container associated to the service."""
if self.action is not None:
self.action.exec(self.id)
class ServicesManager:
"""
A ServicesManager handles a collection of services and forwards certicate update
requests to a CertUpdater. If a service certificate is updated (either after adding a
service or after an explicit update request), the docker action associated
with the service is executed.
Attribures
----------
services_by_id : dict
A dictionnary of the managed services identified by their container id.
services_by_host : dict
A dictionnary of the managed services identified by the CN of their
certificates.
updater : CertUpdater object
The certificates updater in charge of handling the certificates for the managed
services
Methods
-------
add(service)
Adds a new service to the collection
remove(id)
Removes a service from the collection based on its container id
update()
Checks if any certificate for a managed service needs to be updated. If
so, performs the docker action associated with the service on its
container
"""
def __init__(self, updater):
"""
Parameters
----------
updater : CertUpdater
The CertUpdater used to handle certificates for the services
"""
self.services_by_id = {}
self.services_by_host = {}
self.updater = updater
self.lock = Lock()
def add(self, service):
""" Adds a service to the collection and enables handling of its
certificates
"""
with self.lock:
self.services_by_id[service.id] = service
if service.host not in self.services_by_host:
self.services_by_host[service.host] = [service]
else:
self.services_by_host[service.host].append(service)
self.updater.add(service.host)
self.update()
def remove(self, id):
""" Removes a service by its id from the collection and disables handling
of its certificates
"""
with self.lock:
if id in self.services_by_id:
service = self.services_by_id[id]
host = service.host
self.services_by_host[host].remove(service)
del self.services_by_id[id]
if len(self.services_by_host[host]) == 0:
del self.services_by_host[host]
self.updater.remove(host)
def update(self):
""" Updates the certificates of the services if needed. If a certificate
has changed, exectute the associated docker action on the service container.
"""
with self.lock:
for h in self.updater.update():
for s in self.services_by_host[h]:
s.run_action()
\ No newline at end of file
docker
watchdog
#!/usr/bin/env python3
import sys, os
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../app')
import time
import cert_updater
import file_watcher
import docker_monitor
import service_manager
if __name__ == "__main__":
u = cert_updater.CertUpdater('./acme.json', '.')
sm = service_manager.ServicesManager(u)
f = file_watcher.FileWatcher('./acme.json', sm.update)
dm = docker_monitor.DockerMonitor(sm)
f.start()
dm.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
dm.stop()
f.stop()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment