235 lines
8.1 KiB
Python
235 lines
8.1 KiB
Python
# hobo - portal to configure and deploy applications
|
|
# Copyright (C) 2015 Entr'ouvert
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it
|
|
# under the terms of the GNU Affero General Public License as published
|
|
# by the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
import fnmatch
|
|
import json
|
|
import multiprocessing
|
|
import multiprocessing.pool
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import urllib.parse
|
|
|
|
from django.utils.encoding import force_bytes
|
|
|
|
from . import settings
|
|
|
|
|
|
class BaseService:
|
|
tenants_dirs = None
|
|
|
|
def __init__(self, base_url, title, secret_key, **kwargs):
|
|
self.base_url = base_url
|
|
self.title = title
|
|
self.secret_key = secret_key
|
|
|
|
@classmethod
|
|
def get_actual_tenants(cls):
|
|
for tenants_dir in cls.tenants_dirs:
|
|
if os.path.exists(tenants_dir):
|
|
yield from os.listdir(tenants_dir)
|
|
|
|
@classmethod
|
|
def is_for_us(cls, url):
|
|
# This function checks if the requested service is to be hosted
|
|
# on this server, and return True if appropriate.
|
|
#
|
|
# It matches against a set of patterns taken from
|
|
# settings.AGENT_HOST_PATTERNS; patterns can be full hostname or use
|
|
# globbing characters (ex: "*.au-quotidien.com"); it is also possible
|
|
# to # prefix the pattern by an exclamation mark to exclude those ones
|
|
# (ex: "! *.dev.au-quotidien.com").
|
|
if not settings.AGENT_HOST_PATTERNS:
|
|
return True
|
|
patterns = settings.AGENT_HOST_PATTERNS.get(cls.service_id)
|
|
if patterns is None:
|
|
return True
|
|
parsed_url = urllib.parse.urlsplit(url)
|
|
netloc = parsed_url.netloc
|
|
match = False
|
|
for pattern in patterns:
|
|
if pattern.startswith('!'):
|
|
if fnmatch.fnmatch(netloc, pattern[1:].strip()):
|
|
match = False
|
|
break
|
|
else:
|
|
if fnmatch.fnmatch(netloc, pattern.strip()):
|
|
match = True
|
|
return match
|
|
|
|
def check_timestamp(self, timestamp):
|
|
'''Return True if site is uptodate'''
|
|
return False
|
|
|
|
def execute(self, environment):
|
|
if not os.path.exists(self.service_manage_try_cmd):
|
|
return
|
|
cmd = self.service_manage_cmd + ' hobo_deploy ' + self.base_url + ' -'
|
|
cmd_process = subprocess.Popen(
|
|
cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
)
|
|
stdout, stderr = cmd_process.communicate(input=force_bytes(json.dumps(environment)))
|
|
if cmd_process.returncode != 0:
|
|
raise RuntimeError('command "%s" failed: %r %r' % (cmd, stdout, stderr))
|
|
|
|
@classmethod
|
|
def notify(cls, data):
|
|
if not os.path.exists(cls.service_manage_try_cmd):
|
|
return
|
|
tenants = list(cls.get_actual_tenants())
|
|
if tenants:
|
|
for audience in data.get('audience', []):
|
|
parsed_url = urllib.parse.urlsplit(audience)
|
|
netloc = parsed_url.netloc.split(':')[0]
|
|
if netloc in tenants:
|
|
break
|
|
else:
|
|
return
|
|
cmd = cls.service_manage_cmd + ' hobo_notify -'
|
|
try:
|
|
cmd_process = subprocess.Popen(
|
|
cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
)
|
|
except OSError:
|
|
return
|
|
stdout, stderr = cmd_process.communicate(input=force_bytes(json.dumps(data)))
|
|
if cmd_process.returncode != 0:
|
|
raise RuntimeError('command "%s" failed: %r %r' % (cmd, stdout, stderr))
|
|
|
|
|
|
class Passerelle(BaseService):
|
|
service_id = 'passerelle'
|
|
service_manage_cmd = settings.PASSERELLE_MANAGE_COMMAND
|
|
service_manage_try_cmd = settings.PASSERELLE_MANAGE_TRY_COMMAND
|
|
tenants_dirs = [settings.PASSERELLE_TENANTS_DIRECTORY]
|
|
|
|
|
|
class Wcs(BaseService):
|
|
service_id = 'wcs'
|
|
service_manage_cmd = settings.WCS_MANAGE_COMMAND
|
|
service_manage_try_cmd = settings.WCS_MANAGE_TRY_COMMAND
|
|
tenants_dirs = [settings.WCS_TENANTS_DIRECTORY, os.path.join(settings.WCS_TENANTS_DIRECTORY, 'tenants')]
|
|
|
|
|
|
class Authentic(BaseService):
|
|
service_id = 'authentic'
|
|
service_manage_cmd = settings.AUTHENTIC_MANAGE_COMMAND
|
|
service_manage_try_cmd = settings.AUTHENTIC_MANAGE_TRY_COMMAND
|
|
tenants_dirs = [settings.AUTHENTIC_TENANTS_DIRECTORY]
|
|
|
|
@classmethod
|
|
def notify(cls, data):
|
|
return
|
|
|
|
|
|
class Chrono(BaseService):
|
|
service_id = 'chrono'
|
|
service_manage_cmd = settings.CHRONO_MANAGE_COMMAND
|
|
service_manage_try_cmd = settings.CHRONO_MANAGE_TRY_COMMAND
|
|
tenants_dirs = [settings.CHRONO_TENANTS_DIRECTORY]
|
|
|
|
|
|
class Combo(BaseService):
|
|
service_id = 'combo'
|
|
service_manage_cmd = settings.COMBO_MANAGE_COMMAND
|
|
service_manage_try_cmd = settings.COMBO_MANAGE_TRY_COMMAND
|
|
tenants_dirs = [settings.COMBO_TENANTS_DIRECTORY]
|
|
|
|
|
|
class Fargo(BaseService):
|
|
service_id = 'fargo'
|
|
service_manage_cmd = settings.FARGO_MANAGE_COMMAND
|
|
service_manage_try_cmd = settings.FARGO_MANAGE_TRY_COMMAND
|
|
tenants_dirs = [settings.FARGO_TENANTS_DIRECTORY]
|
|
|
|
|
|
class Hobo(BaseService):
|
|
service_id = 'hobo'
|
|
service_manage_cmd = settings.HOBO_MANAGE_COMMAND
|
|
service_manage_try_cmd = settings.HOBO_MANAGE_TRY_COMMAND
|
|
tenants_dirs = [settings.HOBO_TENANTS_DIRECTORY]
|
|
|
|
|
|
class Lingo(BaseService):
|
|
service_id = 'lingo'
|
|
service_manage_cmd = settings.LINGO_MANAGE_COMMAND
|
|
service_manage_try_cmd = settings.LINGO_MANAGE_TRY_COMMAND
|
|
tenants_dirs = [settings.LINGO_TENANTS_DIRECTORY]
|
|
|
|
|
|
class Welco(BaseService):
|
|
service_id = 'welco'
|
|
service_manage_cmd = settings.WELCO_MANAGE_COMMAND
|
|
service_manage_try_cmd = settings.WELCO_MANAGE_TRY_COMMAND
|
|
tenants_dirs = [settings.WELCO_TENANTS_DIRECTORY]
|
|
|
|
|
|
class BiJoe(BaseService):
|
|
service_id = 'bijoe'
|
|
service_manage_cmd = settings.BIJOE_MANAGE_COMMAND
|
|
service_manage_try_cmd = settings.BIJOE_MANAGE_TRY_COMMAND
|
|
tenants_dirs = [settings.BIJOE_TENANTS_DIRECTORY]
|
|
|
|
|
|
def deploy(environment):
|
|
if 'DJANGO_SETTINGS_MODULE' in os.environ:
|
|
# remove environment variable so the management commands of the other
|
|
# projects don't find themselves loading hobo settings.py...
|
|
del os.environ['DJANGO_SETTINGS_MODULE']
|
|
|
|
hobo_timestamp = environment.get('timestamp')
|
|
|
|
service_classes = {}
|
|
|
|
for klassname, service in globals().items():
|
|
if not hasattr(service, 'service_id'):
|
|
continue
|
|
service_classes[service.service_id] = service
|
|
|
|
services = []
|
|
for service in environment.get('services', []):
|
|
service_id = service.get('service-id')
|
|
if not service_id in service_classes:
|
|
continue
|
|
if not 'secret_key' in service:
|
|
# the hobo instance that emit the environement message does not a have a secret key
|
|
continue
|
|
service_obj = service_classes.get(service_id)(**service)
|
|
if not service_obj.is_for_us(service_obj.base_url):
|
|
print('skipping as not for us: %r' % service_obj.base_url, file=sys.stderr)
|
|
continue
|
|
if service_obj.check_timestamp(hobo_timestamp):
|
|
print('skipping uptodate site: %r' % service_obj.base_url, file=sys.stderr)
|
|
continue
|
|
services.append(service_obj)
|
|
pool = multiprocessing.pool.ThreadPool()
|
|
list(pool.imap_unordered(lambda x: x.execute(environment), services))
|
|
|
|
|
|
def notify(data):
|
|
services = []
|
|
for klassname, service in globals().items():
|
|
if not hasattr(service, 'service_id'):
|
|
continue
|
|
services.append(service)
|
|
pool = multiprocessing.pool.ThreadPool()
|
|
try:
|
|
list(pool.imap_unordered(lambda x: x.notify(data), services))
|
|
finally:
|
|
pool.close()
|
|
pool.terminate()
|