198 lines
7.5 KiB
Python
198 lines
7.5 KiB
Python
import json
|
|
import os
|
|
import requests
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import urlparse
|
|
|
|
from django.conf import settings
|
|
from django.core.management.base import BaseCommand, CommandError
|
|
from django.core.management import call_command, get_commands
|
|
|
|
from tenant_schemas.utils import tenant_context
|
|
from hobo.multitenant.middleware import TenantMiddleware, TenantNotFound
|
|
from hobo.theme.utils import get_theme
|
|
|
|
# TODO: move this to settings
|
|
KEY_SIZE = 1024
|
|
DAYS = 3652
|
|
|
|
|
|
def replace_file(path, content):
|
|
dirname = os.path.dirname(path)
|
|
fd, temp = tempfile.mkstemp(dir=dirname,
|
|
prefix='.tmp-'+os.path.basename(path)+'-')
|
|
f = os.fdopen(fd, 'w')
|
|
f.write(content)
|
|
f.flush()
|
|
os.fsync(f.fileno())
|
|
f.close()
|
|
os.rename(temp, path)
|
|
|
|
|
|
class Command(BaseCommand):
|
|
early_secondary_exit = True
|
|
me = None
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument('base_url', metavar='BASE_URL', nargs='?', type=str)
|
|
parser.add_argument('json_filename', metavar='JSON_FILENAME', nargs='?', type=str)
|
|
parser.add_argument('--ignore-timestamp', dest='ignore_timestamp',
|
|
action="store_true", default=False)
|
|
parser.add_argument('--redeploy', action="store_true", default=False)
|
|
|
|
def handle(self, base_url=None, json_filename=None, ignore_timestamp=None,
|
|
redeploy=None, *args, **kwargs):
|
|
if redeploy:
|
|
for tenant in TenantMiddleware.get_tenants():
|
|
try:
|
|
hobo_environment = tenant.get_hobo_json()
|
|
except IOError:
|
|
continue
|
|
try:
|
|
me = [x for x in hobo_environment.get('services') if x.get('this') is True][0]
|
|
except IndexError:
|
|
continue
|
|
self.deploy(me['base_url'], hobo_environment, True)
|
|
print 'Redeployed', me['base_url']
|
|
else:
|
|
if not base_url or not json_filename:
|
|
raise CommandError('missing args')
|
|
if json_filename == '-':
|
|
hobo_environment = json.load(sys.stdin)
|
|
else:
|
|
hobo_environment = json.load(file(json_filename))
|
|
self.deploy(base_url, hobo_environment, ignore_timestamp)
|
|
|
|
def deploy(self, base_url, hobo_environment, ignore_timestamp):
|
|
self.me = [x for x in hobo_environment.get('services') if x.get('base_url') == base_url][0]
|
|
if self.me.get('secondary') and self.early_secondary_exit:
|
|
# early exit, we don't redeploy secondary services
|
|
return
|
|
domain = urlparse.urlparse(self.me.get('base_url')).netloc.split(':')[0]
|
|
|
|
try:
|
|
tenant = TenantMiddleware.get_tenant_by_hostname(domain)
|
|
except TenantNotFound:
|
|
# create tenant for domain
|
|
call_command('create_tenant', domain)
|
|
tenant = TenantMiddleware.get_tenant_by_hostname(domain)
|
|
|
|
timestamp = hobo_environment.get('timestamp')
|
|
tenant_hobo_json = os.path.join(tenant.get_directory(), 'hobo.json')
|
|
if os.path.exists(tenant_hobo_json):
|
|
if not ignore_timestamp and json.load(file(tenant_hobo_json)).get('timestamp') == timestamp:
|
|
return
|
|
|
|
# add an attribute to current tenant for easier retrieval
|
|
self.me['this'] = True
|
|
|
|
self.deploy_specifics(hobo_environment, tenant)
|
|
|
|
if not self.me.get('secondary'):
|
|
replace_file(tenant_hobo_json, json.dumps(hobo_environment, indent=2))
|
|
|
|
def deploy_specifics(self, hobo_environment, tenant):
|
|
# configure things that are quite common but may actually not be
|
|
# common to *all* tenants.
|
|
self.generate_saml_keys(tenant, prefix='sp-')
|
|
self.configure_service_provider(hobo_environment, tenant)
|
|
self.configure_theme(hobo_environment, tenant)
|
|
self.configure_template(hobo_environment, tenant)
|
|
|
|
def generate_saml_keys(self, tenant, prefix=''):
|
|
|
|
def openssl(*args):
|
|
with open('/dev/null', 'w') as dev_null:
|
|
subprocess.check_call(['openssl'] + list(args), stdout=dev_null, stderr=dev_null)
|
|
|
|
key_file = os.path.join(tenant.get_directory(), '%ssaml.key' % prefix)
|
|
cert_file = os.path.join(tenant.get_directory(), '%ssaml.crt' % prefix)
|
|
|
|
# if files exist don't regenerate them
|
|
if os.path.exists(key_file) and os.path.exists(cert_file):
|
|
return
|
|
|
|
openssl('req', '-x509', '-sha256', '-newkey', 'rsa:%s' % KEY_SIZE, '-nodes',
|
|
'-keyout', key_file, '-out', cert_file, '-batch',
|
|
'-subj', '/CN=%s' % tenant.domain_url[:60], '-days', str(DAYS))
|
|
|
|
def configure_service_provider(self, hobo_environment, tenant):
|
|
# configure authentication against identity provider
|
|
for service in hobo_environment.get('services'):
|
|
idp_url = service.get('saml-idp-metadata-url')
|
|
if not idp_url:
|
|
continue
|
|
try:
|
|
response = requests.get(idp_url, verify=False)
|
|
except requests.exceptions.RequestException:
|
|
continue
|
|
if response.status_code != 200:
|
|
continue
|
|
tenant_idp_metadata = os.path.join(tenant.get_directory(),
|
|
'idp-metadata-%s.xml' % service.get('id'))
|
|
replace_file(tenant_idp_metadata, response.content)
|
|
# break now, only a single IdP is supported
|
|
break
|
|
|
|
def get_theme(self, hobo_environment):
|
|
theme_id = None
|
|
if self.me:
|
|
theme_id = self.me.get('variables', {}).get('theme')
|
|
if not theme_id:
|
|
theme_id = hobo_environment.get('variables', {}).get('theme')
|
|
if not theme_id:
|
|
return
|
|
theme = get_theme(theme_id)
|
|
if not theme:
|
|
return
|
|
if not theme.get('module'):
|
|
return
|
|
return theme
|
|
|
|
def configure_theme(self, hobo_environment, tenant):
|
|
theme = self.get_theme(hobo_environment)
|
|
if not theme:
|
|
return
|
|
|
|
tenant_dir = tenant.get_directory()
|
|
theme_dir = os.path.join(tenant_dir, 'theme')
|
|
target_dir = os.path.join(settings.THEMES_DIRECTORY,
|
|
theme.get('module'))
|
|
|
|
def atomic_symlink(src, dst):
|
|
if os.path.exists(dst) and os.readlink(dst) == src:
|
|
return
|
|
if os.path.exists(dst + '.tmp'):
|
|
os.unlink(dst + '.tmp')
|
|
os.symlink(src, dst + '.tmp')
|
|
os.rename(dst + '.tmp', dst)
|
|
|
|
atomic_symlink(target_dir, theme_dir)
|
|
|
|
for part in ('static', 'templates'):
|
|
if not os.path.islink(os.path.join(tenant_dir, part)) and \
|
|
os.path.isdir(os.path.join(tenant_dir, part)):
|
|
try:
|
|
os.rmdir(os.path.join(tenant_dir, part))
|
|
except OSError:
|
|
continue
|
|
|
|
if not theme.get('overlay'):
|
|
try:
|
|
os.unlink(os.path.join(tenant_dir, part))
|
|
except OSError:
|
|
pass
|
|
else:
|
|
target_dir = os.path.join(settings.THEMES_DIRECTORY,
|
|
theme.get('overlay'), part)
|
|
atomic_symlink(target_dir, os.path.join(tenant_dir, part))
|
|
|
|
def configure_template(self, hobo_environment, tenant):
|
|
me = [x for x in hobo_environment.get('services') if x.get('this') is True][0]
|
|
|
|
if 'import_template' in get_commands() and me.get('template_name'):
|
|
with tenant_context(tenant):
|
|
call_command('import_template', me['template_name'])
|