hobo/hobo/environment/management/commands/cook.py

333 lines
13 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 json
import os
import string
import subprocess
import urllib.parse
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.management import call_command
from django.core.management.base import BaseCommand, CommandError
from django.db import connection
from django.db.models import Max
from django.utils.text import slugify
from hobo.agent.common.management.commands.hobo_deploy import Command as HoboDeployCommand
from hobo.deploy.signals import notify_agents
from hobo.environment.models import (
AUTO_VARIABLES,
AVAILABLE_SERVICES,
Authentic,
BiJoe,
Chrono,
Combo,
Fargo,
Hobo,
Lingo,
Passerelle,
Variable,
Wcs,
Welco,
)
from hobo.environment.utils import wait_operationals
from hobo.environment.validators import validate_service_url
from hobo.multitenant.middleware import TenantMiddleware
from hobo.profile.models import AttributeDefinition
from hobo.theme.utils import set_theme
def get_domain(url):
return urllib.parse.urlparse(url).netloc.split(':')[0]
class Command(BaseCommand):
permissive = False
must_notify = False
verbosity = 1
def add_arguments(self, parser):
parser.add_argument('recipe', metavar='RECIPE', type=str)
parser.add_argument(
'--timeout',
type=int,
action='store',
default=120,
help='set the timeout for the wait_operationals method',
)
parser.add_argument('--permissive', action='store_true', help='ignore integrity checks')
def handle(self, recipe, *args, **kwargs):
self.verbosity = kwargs.get('verbosity')
self.timeout = kwargs.get('timeout')
self.permissive = kwargs.get('permissive')
self.terminal_width = 0
if self.verbosity > 1:
try:
self.terminal_width = int(subprocess.check_output(['tput', 'cols']).strip())
except OSError:
self.terminal_width = 80
self.run_cook(recipe)
if self.verbosity:
print('All steps executed successfully. Your environment should now be ready.')
def run_cook(self, filename):
recipe = json.load(open(filename))
variables = {}
steps = []
if 'load-variables-from' in recipe:
variables.update(
json.load(open(os.path.join(os.path.dirname(filename), recipe['load-variables-from'])))
)
variables.update(recipe.get('variables', {}))
for step in recipe.get('steps', []):
action, action_args = list(step.items())[0]
for arg in action_args:
if not isinstance(action_args[arg], str):
continue
action_args[arg] = string.Template(action_args[arg]).substitute(variables)
if not self.permissive:
self.check_action(action, action_args)
steps.append((action, action_args))
for action, action_args in steps:
getattr(self, action.replace('-', '_'))(**action_args)
if self.must_notify:
notify_agents(None)
self.wait_operationals(timeout=self.timeout)
self.must_notify = False
notify_agents(None)
def wait_operationals(self, timeout):
services = []
for service_class in AVAILABLE_SERVICES:
services.extend(service_class.objects.all())
wait_operationals(services, timeout, self.verbosity, self.terminal_width, notify_agents)
def create_hobo(self, url, primary=False, title=None, slug=None, **kwargs):
if connection.tenant.schema_name == 'public':
# if we're not currently in a tenant then we force the creation of
# a primary hobo
primary = True
if not primary:
if not slug:
slug = 'hobo-%s' % slugify(title)
self.create_site(Hobo, url, title, slug, template_name='', variables=None)
# deploy and wait for new site
notify_agents(None)
self.wait_operationals(timeout=self.timeout)
# switch context to new hobo
domain = get_domain(url)
tenant = TenantMiddleware.get_tenant_by_hostname(domain)
connection.set_tenant(tenant)
return
domain = get_domain(url)
try:
call_command('create_hobo_tenant', domain)
except CommandError:
pass
tenant = TenantMiddleware.get_tenant_by_hostname(domain)
base_url_filename = os.path.join(tenant.get_directory(), 'base_url')
if not os.path.exists(base_url_filename):
fd = open(base_url_filename, 'w')
fd.write(url.rstrip('/'))
fd.close()
connection.set_tenant(tenant)
def create_superuser(
self, username='admin', email='admin@localhost', first_name='Admin', last_name='', password=None
):
user, created = User.objects.get_or_create(username=username, email=email, is_superuser=True)
if created:
user.first_name = first_name
user.last_name = last_name
password = password or User.objects.make_random_password(length=30)
if password:
user.set_password(password)
user.save()
if created and self.verbosity:
print('superuser account: %s / %s' % (username, password))
def create_site(self, klass, base_url, title, slug, template_name, variables):
if slug is None:
slug = klass.Extra.service_default_slug
try:
obj = klass.objects.get(slug=slug)
must_save = False
except klass.DoesNotExist:
obj = klass(slug=slug)
must_save = True
for attr in ('title', 'base_url', 'template_name'):
if getattr(obj, attr) != locals().get(attr):
setattr(obj, attr, locals().get(attr))
must_save = True
try:
obj.full_clean(
exclude=[
'secret_key',
'last_operational_success_timestamp',
'last_operational_check_timestamp',
]
)
except ValidationError as e:
raise CommandError(str(e))
if must_save:
obj.save()
self.must_notify = True
variables = variables or {}
obj_type = ContentType.objects.get_for_model(klass)
for variable_name in variables.keys():
label = variables[variable_name].get('label')
variable, created = Variable.objects.get_or_create(
name=variable_name,
service_type=obj_type,
service_pk=obj.id,
defaults={'label': label or variable_name},
)
if label:
variable.label = label
value = variables[variable_name].get('value')
if isinstance(value, dict) or isinstance(value, list):
value = json.dumps(value)
variable.value = value
variable.save()
def create_authentic(self, url, title, slug=None, template_name='', variables=None, **kwargs):
return self.create_site(Authentic, url, title, slug, template_name, variables)
def set_idp(self, url=None):
if url:
obj = Authentic.objects.get(base_url=url)
else:
obj = Authentic.objects.all()[0]
if not obj.use_as_idp_for_self:
obj.use_as_idp_for_self = True
obj.save()
def create_combo(self, url, title, slug=None, template_name='', variables=None, **kwargs):
return self.create_site(Combo, url, title, slug, template_name, variables)
def create_wcs(self, url, title, slug=None, template_name='', variables=None, **kwargs):
return self.create_site(Wcs, url, title, slug, template_name, variables)
def create_passerelle(self, url, title, slug=None, template_name='', variables=None, **kwargs):
return self.create_site(Passerelle, url, title, slug, template_name, variables)
def create_fargo(self, url, title, slug=None, template_name='', variables=None, **kwargs):
return self.create_site(Fargo, url, title, slug, template_name, variables)
def create_welco(self, url, title, slug=None, template_name='', variables=None, **kwargs):
return self.create_site(Welco, url, title, slug, template_name, variables)
def create_chrono(self, url, title, slug=None, template_name='', variables=None, **kwargs):
return self.create_site(Chrono, url, title, slug, template_name, variables)
def create_lingo(self, url, title, slug=None, template_name='', variables=None, **kwargs):
return self.create_site(Lingo, url, title, slug, template_name, variables)
def create_bijoe(self, url, title, slug=None, template_name='', variables=None, **kwargs):
return self.create_site(BiJoe, url, title, slug, template_name, variables)
def set_theme(self, theme):
set_theme(theme)
HoboDeployCommand().configure_theme({'variables': {'theme': theme}}, connection.tenant)
def set_variable(self, name, value, label=None, auto=None):
if auto is None:
auto = bool(name in AUTO_VARIABLES)
else:
auto = bool(auto)
variable, created = Variable.objects.get_or_create(
name=name, defaults={'label': label or name, 'auto': auto}
)
if isinstance(value, dict) or isinstance(value, list):
value = json.dumps(value)
if variable.label != label or variable.value != value or variable.auto != auto or created:
if label:
variable.label = label
variable.auto = auto
variable.value = value
variable.save()
def enable_attribute(self, name):
try:
attribute = AttributeDefinition.objects.get(name=name)
except AttributeDefinition.DoesNotExist:
return
if attribute.disabled:
attribute.disabled = False
attribute.save()
def disable_attribute(self, name):
try:
attribute = AttributeDefinition.objects.get(name=name)
except AttributeDefinition.DoesNotExist:
return
if not attribute.disabled:
attribute.disabled = True
attribute.save()
def set_attribute(self, name, label, **kwargs):
# possible keys in kwargs are: description, required,
# asked_on_registration, user_editable, user_visible, kind, order
attribute, created = AttributeDefinition.objects.get_or_create(
name=name, defaults={'label': label, 'order': 0}
)
kwargs['label'] = label
attribute_fields = [x.name for x in AttributeDefinition._meta.fields]
for arg in kwargs:
if arg in attribute_fields:
setattr(attribute, arg, kwargs.get(arg))
if created and not attribute.order:
attribute.order = AttributeDefinition.objects.all().aggregate(Max('order')).get('order__max') + 1
attribute.save()
def cook(self, filename):
current_tenant = connection.tenant
self.run_cook(filename)
connection.set_tenant(current_tenant)
def check_action(self, action, action_args):
method_name = action.replace('-', '_')
if not hasattr(self, method_name):
raise CommandError('Error: Unknown action %s' % action)
if method_name.startswith('create_'):
service_class_name = method_name.split('create_', 1)[1]
for service_class in AVAILABLE_SERVICES:
if service_class.__name__.lower() == service_class_name:
if not service_class.is_enabled():
raise CommandError(f'{action}: service class "{service_class.__name__}" is disabled')
break
if 'url' in action_args.keys():
try:
validate_service_url(action_args['url'])
except ValidationError as e:
raise CommandError(e)