hobo/hobo/agent/common/management/commands/hobo_deploy.py

197 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))
self.configure_template(hobo_environment, tenant)
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)
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
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'])