hobo/hobo/agent/worker/services.py

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()