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

332 lines
13 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/>.
from __future__ import print_function
import json
import string
import subprocess
import sys
import time
import os
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand, CommandError
from django.core.management import call_command
from django.db import connection
from django.db.models import Max
from django.core.exceptions import ValidationError
from django.utils import six
from django.utils.six.moves.urllib import parse as urlparse
from django.utils.text import slugify
from hobo.agent.common.management.commands.hobo_deploy import (
Command as HoboDeployCommand)
from hobo.multitenant.middleware import TenantMiddleware
from hobo.environment.models import (AVAILABLE_SERVICES, Authentic, Wcs, Hobo,
Passerelle, Combo, Fargo, Welco, Chrono, BiJoe,
Variable, AUTO_VARIABLES)
from hobo.environment.validators import validate_service_url
from hobo.deploy.signals import notify_agents
from hobo.theme.utils import set_theme
from hobo.profile.models import AttributeDefinition
def get_domain(url):
return urlparse.urlparse(url).netloc.split(':')[0]
class Command(BaseCommand):
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')
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], six.string_types):
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())
t0 = time.time()
i = 0
last_service_url = None
last_notification = t0
while len(services) > 0:
if time.time() - last_notification > 15:
last_notification = time.time()
notify_agents(None)
for service in services[:]:
if service.last_operational_success_timestamp:
services.remove(service)
continue
service.check_operational()
if len(services) == 0:
break
if self.verbosity == 1:
sys.stderr.write('.')
elif self.verbosity > 1:
if last_service_url != services[0].base_url:
last_service_url = services[0].base_url
i = 0
elif i == (self.terminal_width - len(services[0].base_url) - 25):
i = 0
i += 1
sys.stderr.write('\rWaiting for %s ' % services[0].base_url)
sys.stderr.write('%5ds ' % (timeout - (time.time() - t0)))
sys.stderr.write('.' * i)
sys.stderr.flush()
time.sleep(0.5)
if time.time() - t0 > timeout:
if self.verbosity:
sys.stderr.write('\n')
raise CommandError('timeout waiting for %s' % ', '.join(
[x.base_url for x in services]))
def create_hobo(self, url, primary=False, title=None, slug=None, **kwargs):
if connection.get_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
obj, must_save = klass.objects.get_or_create(
slug=slug,
defaults={
'title': title,
'base_url': base_url,
'template_name': template_name
})
for attr in ('title', 'base_url', 'template_name'):
if getattr(obj, attr) != locals().get(attr):
setattr(obj, attr, locals().get(attr))
must_save = True
if must_save:
try:
obj.full_clean(exclude=['last_operational_success_timestamp', 'last_operational_check_timestamp'])
except ValidationError as e:
raise CommandError(str(e))
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_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.get_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.get_tenant()
self.run_cook(filename)
connection.set_tenant(current_tenant)
def check_action(self, action, action_args):
if not hasattr(self, action.replace('-', '_')):
raise CommandError('Error: Unknown action %s' % action)
if 'url' in action_args.keys():
try:
validate_service_url(action_args['url'])
except ValidationError as e:
raise CommandError(e)