hobo/hobo/contrib/ozwillo/models.py

286 lines
11 KiB
Python

# Ozwillo plugin to deploy Publik
# Copyright (C) 2017 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 logging
import os
import subprocess
import tempfile
import json
import requests
from django.conf import settings
from django.db import models
from django.db.transaction import atomic
from django.utils.text import slugify
from django.core.management import call_command
from django.db import connection
from tenant_schemas.utils import tenant_context
class OzwilloInstance(models.Model):
STATE_NEW = 'new'
STATE_TO_DEPLOY = 'to_deploy'
STATE_DEPLOY_ERROR = 'deploy_error'
STATE_DEPLOYED = 'deployed'
STATE_TO_DESTROY = 'to_destroy'
STATE_DESTROY_ERROR = 'destroy_error'
STATE_DESTROYED = 'destroyed'
STATES = [
(STATE_NEW, 'new'),
(STATE_TO_DEPLOY, 'to_deploy'),
(STATE_DEPLOYED, 'deployed'),
(STATE_DEPLOY_ERROR, 'deploy_error'),
(STATE_TO_DESTROY, 'to_destroy'),
(STATE_DESTROY_ERROR, 'destroy_error'),
(STATE_DESTROYED, 'destroyed'),
]
state = models.CharField(max_length=16, default=STATE_NEW, choices=STATES)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
domain_slug = models.CharField(max_length=250)
external_ozwillo_id = models.CharField(max_length=450, unique=True)
deploy_data = models.TextField(null=True)
def __unicode__(self):
return self.domain_slug
def __repr__(self):
return '<OzwilloInstance external_ozwillo_id: %r domain_slug: %r state: %r>' % (
self.external_ozwillo_id, self.domain_slug, self.state)
@property
def data(self):
return json.loads(self.deploy_data) if self.deploy_data else None
def to_deploy(self):
assert self.state == self.STATE_NEW
try:
with atomic():
# lock the new instance to prevent collision with the CRON job
OzwilloInstance.objects.select_for_update().filter(id=self.id)
# instance starts with the NEW state, to prevent the CRON from
# deploying instance juste created
# only instance in the state STATE_TO_DEPLOY are deployed.
self.state = self.STATE_TO_DEPLOY
self.save()
self.deploy()
except Exception:
# something failed, still make the instance to be deployed
# an reraise exception
self.state = self.STATE_TO_DEPLOY
self.save()
raise
def to_destroy(self):
assert self.state == self.STATE_DEPLOYED
self.state = self.STATE_TO_DESTROY
self.save()
def deploy_error(self):
assert self.state in [self.STATE_DEPLOY_ERROR, self.STATE_TO_DEPLOY]
if self.state == self.STATE_TO_DEPLOY:
self.state = self.STATE_DEPLOY_ERROR
self.save()
def deploy(self):
logger.info(u'ozwillo: deploy start for %s', self)
data = self.data
if not data:
logger.warning(u'ozwillo: unable to deploy, no data')
return
# Request parsing
client_id = data['client_id']
client_secret = data['client_secret']
instance_id = data['instance_id']
instance_name = data['organization_name']
instance_name = slugify(instance_name)
registration_uri = data['instance_registration_uri']
user = data['user']
# Cook new platform
template_recipe = json.load(open('/etc/hobo/ozwillo/template_recipe.json', 'rb'))
var = template_recipe['variables']
for key, value in var.items():
var[key] = value.replace('instance_name', instance_name)
template_recipe['variables'] = var
domain = var['combo']
domain_agent = var['combo_agent']
domain_passerelle = var['passerelle']
logger.info(u'ozwillo: cooking %s', template_recipe)
with tempfile.NamedTemporaryFile() as recipe_file:
json.dump(template_recipe, recipe_file)
recipe_file.flush()
# cook play with the tenant context, we must protect from that
with tenant_context(connection.tenant):
call_command('cook', recipe_file.name, timeout=1000, verbosity=0)
# Load user portal template
logger.info(u'ozwillo: loading combo template')
run_command([
'sudo', '-u', 'combo',
'combo-manage', 'tenant_command', 'import_site',
'/etc/hobo/ozwillo/import-site-template.json',
'-d', domain
])
# Load agent portal template
logger.info(u'ozwillo: loading combo agent template')
run_command([
'sudo', '-u', 'combo',
'combo-manage', 'tenant_command', 'import_site',
'/etc/hobo/ozwillo/import-site-agents.json',
'-d', domain_agent
])
# Configure OIDC Ozwillo authentication
logger.info(u'ozwillo: configuring OIDC ozwillo authentication')
domain_name = 'connexion-%s.%s' % (instance_name, settings.OZWILLO_ENV_DOMAIN)
if run_command([
'sudo', '-u', 'authentic-multitenant',
'authentic2-multitenant-manage', 'tenant_command', 'oidc-register-issuer',
'-d', domain_name,
'--scope', 'profile',
'--scope', 'email',
'--issuer', settings.OZWILLO_PLATEFORM,
'--client-id', client_id,
'--client-secret', client_secret,
'--claim-mapping', 'given_name first_name always_verified',
'--claim-mapping', 'family_name last_name always_verified',
'--ou-slug', 'default',
'--claim-mapping', 'email email required',
'Ozwillo'
]):
# creation of the admin user depends upon the creation of provider
logger.info(u'ozwillo: creating admin user')
create_user_script = os.path.dirname(__file__) + '/scripts/create_user_ozwillo.py'
run_command([
'sudo', '-u', 'authentic-multitenant',
'authentic2-multitenant-manage', 'tenant_command', 'runscript', '-d', domain_name,
create_user_script, user['email_address'], user['id'], user['name']
])
# Load passerelle template
logger.info(u'ozwillo: loading passerelle template')
run_command([
'sudo', '-u', 'passerelle',
'passerelle-manage', 'tenant_command', 'import_site',
'/etc/hobo/ozwillo/import-site-passerelle.json',
'--import-user', '-d', domain_passerelle
])
# Sending done event to Ozwillo
services = {
'services': [{
'local_id': 'publik',
'name': 'Publik - %s' % (instance_name),
'service_uri': 'https://connexion-%s.%s/accounts/oidc/login?iss=%s'
% (instance_name, settings.OZWILLO_ENV_DOMAIN,
settings.OZWILLO_PLATEFORM),
'description': 'Gestion de la relation usagers',
'tos_uri': 'https://publik.entrouvert.com/',
'policy_uri': 'https://publik.entrouvert.com/',
'icon': 'https://publik.entrouvert.com/static/img/logo-publik-64x64.png',
'payment_option': 'FREE',
'target_audience': ['PUBLIC_BODIES',
'CITIZENS',
'COMPANIES'],
'contacts': ['https://publik.entrouvert.com/'],
'redirect_uris': ['https://connexion-%s.%s/accounts/oidc/callback/'
% (instance_name, settings.OZWILLO_ENV_DOMAIN)],
}],
'instance_id': instance_id,
'destruction_uri': settings.OZWILLO_DESTRUCTION_URI,
'destruction_secret': settings.OZWILLO_DESTRUCTION_SECRET,
'needed_scopes': []
}
logger.info(u'ozwillo: sending registration request, %r', services)
headers = {'Content-type': 'application/json', 'Accept': 'application/json'}
response = requests.post(
registration_uri,
data=json.dumps(services),
auth=(client_id, client_secret),
headers=headers)
logger.info(u'ozwillo: registration response, status=%s content=%r',
response.status_code, response.content)
self.state = self.STATE_DEPLOYED
self.save()
logger.info(u'ozwillo: deploy finished')
def destroy_error(self):
assert self.state in [self.STATE_DESTROY_ERROR, self.STATE_TO_DESTROY]
if self.state == self.STATE_TO_DESTROY:
self.state = self.STATE_DESTROY_ERROR
self.save()
def destroy(self):
logger.info(u'ozwillo: destroy start for %s', self)
instance_slug = self.domain_slug
services = settings.OZWILLO_SERVICES.copy()
# w.c.s. is handled differently
wcs = services.pop('wcs')
for s, infos in services.items():
# to get the two combo instances which have same service
service_user = s.split('_')[0]
tenant = '%s%s.%s' % (infos[0], instance_slug, settings.OZWILLO_ENV_DOMAIN)
run_command([
'sudo', '-u', service_user, infos[1],
'delete_tenant', '--force-drop', tenant
])
tenant = '%s%s.%s' % (wcs[0], instance_slug, settings.OZWILLO_ENV_DOMAIN)
run_command([
'sudo', '-u', 'wcs',
wcs[1], '-f', '/etc/wcs/wcs-au-quotidien.cfg',
'delete_tenant', '--force-drop', tenant
])
logger.info(u'ozwillo: destroy finished')
self.state = self.STATE_DESTROYED
self.save()
def run_command(args):
try:
process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except OSError as e:
logger.error('ozwillo: launching subprocess %s raised error %s', args, e)
return False
logger.info('ozwillo: launching subprocess with pid %s : %s', process.pid, args)
stdoutdata, stderrdata = process.communicate()
if process.returncode != 0:
logger.error('ozwillo: subprocess %s failed returncode=%s stdout=%r stderr=%r',
process.pid, process.returncode, stdoutdata, stderrdata)
return False
logger.info('ozwillo: subprocess terminated')
return True
logger = logging.getLogger(__name__)