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

219 lines
8.1 KiB
Python

import json
import os
import subprocess
import sys
import tempfile
import urllib.parse
import requests
from django.conf import settings
from django.core.management import call_command, get_commands
from django.core.management.base import BaseCommand, CommandError
from django.utils.encoding import force_str
from tenant_schemas.utils import tenant_context
from hobo.multitenant.middleware import TenantMiddleware, TenantNotFound
from hobo.theme.utils import get_theme
KEY_SIZE = 2048
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(force_str(content))
f.flush()
os.fsync(f.fileno())
f.close()
os.rename(temp, path)
class Command(BaseCommand):
early_secondary_exit = True
requires_system_checks = []
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
):
self.redeploy = redeploy
if redeploy:
for tenant in TenantMiddleware.get_tenants():
try:
hobo_environment = tenant.get_hobo_json()
except OSError:
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(open(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 = urllib.parse.urlparse(self.me.get('base_url')).netloc.split(':')[0]
legacy_domain = None
try:
tenant = TenantMiddleware.get_tenant_by_hostname(domain)
except TenantNotFound:
# might be a domain change request
for legacy_urls in self.me.get('legacy_urls', []):
old_domain = urllib.parse.urlparse(legacy_urls['base_url']).netloc.split(':')[0]
try:
tenant = TenantMiddleware.get_tenant_by_hostname(old_domain)
legacy_domain = old_domain
break
except TenantNotFound:
pass
call_command('create_tenant', domain, legacy_hostname=legacy_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(open(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.text)
# 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'])