140 lines
5.6 KiB
Python
140 lines
5.6 KiB
Python
# hobo - portal to configure and deploy applications
|
|
# Copyright (C) 2015-2022 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 io
|
|
import json
|
|
import tarfile
|
|
import urllib.parse
|
|
|
|
from django.conf import settings
|
|
from django.contrib.postgres.fields import JSONField
|
|
from django.db import models
|
|
from django.utils.text import slugify
|
|
from django.utils.timezone import now
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
from hobo.environment.utils import get_installed_services
|
|
from hobo.signature import sign_url
|
|
|
|
from .utils import Requests
|
|
|
|
requests = Requests()
|
|
|
|
|
|
class Application(models.Model):
|
|
SUPPORTED_MODULES = ('wcs',)
|
|
|
|
name = models.CharField(max_length=100, verbose_name=_('Name'))
|
|
slug = models.SlugField(max_length=100)
|
|
description = models.TextField(verbose_name=_('Description'), blank=True)
|
|
editable = models.BooleanField(default=True)
|
|
elements = models.ManyToManyField('Element', blank=True, through='Relation')
|
|
creation_timestamp = models.DateTimeField(default=now)
|
|
last_update_timestamp = models.DateTimeField(auto_now=True)
|
|
|
|
def __repr__(self):
|
|
return '<Application %s>' % self.slug
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.slug:
|
|
base_slug = slugify(self.name)[:95]
|
|
slug = base_slug
|
|
i = 1
|
|
while Application.objects.filter(slug=slug).exists():
|
|
slug = '%s-%s' % (base_slug, i)
|
|
i += 1
|
|
self.slug = slug
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
class Element(models.Model):
|
|
type = models.CharField(max_length=25, verbose_name=_('Type'))
|
|
slug = models.SlugField(max_length=500, verbose_name=_('Slug'))
|
|
name = models.CharField(max_length=500, verbose_name=_('Name'))
|
|
cache = JSONField(blank=True, default=dict)
|
|
|
|
def __repr__(self):
|
|
return '<Element %s/%s>' % (self.type, self.slug)
|
|
|
|
|
|
class Relation(models.Model):
|
|
application = models.ForeignKey(Application, on_delete=models.CASCADE)
|
|
element = models.ForeignKey(Element, on_delete=models.CASCADE)
|
|
auto_dependency = models.BooleanField(default=False)
|
|
|
|
def __repr__(self):
|
|
return '<Relation %s - %s/%s>' % (self.application.slug, self.element.type, self.element.slug)
|
|
|
|
|
|
class Version(models.Model):
|
|
application = models.ForeignKey(Application, on_delete=models.CASCADE)
|
|
bundle = models.FileField(upload_to='applications', blank=True, null=True)
|
|
creation_timestamp = models.DateTimeField(default=now)
|
|
last_update_timestamp = models.DateTimeField(auto_now=True)
|
|
deployment_status = JSONField(blank=True, default=dict)
|
|
|
|
def __repr__(self):
|
|
return '<Version %s>' % self.application.slug
|
|
|
|
def deploy(self):
|
|
bundle_content = self.bundle.read()
|
|
self.deploy_roles(bundle_content)
|
|
for service_id, services in getattr(settings, 'KNOWN_SERVICES', {}).items():
|
|
if service_id not in Application.SUPPORTED_MODULES:
|
|
continue
|
|
service_objects = {x.get_base_url_path(): x for x in get_installed_services(types=[service_id])}
|
|
for service in services.values():
|
|
if service_objects[service['url']].secondary:
|
|
continue
|
|
url = urllib.parse.urljoin(service['url'], 'api/export-import/bundle-import/')
|
|
response = requests.put(sign_url(url, service['secret']), data=bundle_content)
|
|
if not response.ok:
|
|
# TODO: report failures
|
|
continue
|
|
# TODO: look at response content for afterjob URLs to display a progress bar
|
|
pass
|
|
|
|
def get_authentic_service(self):
|
|
for service_id, services in getattr(settings, 'KNOWN_SERVICES', {}).items():
|
|
if service_id == 'authentic':
|
|
for service in services.values():
|
|
return service
|
|
return None
|
|
|
|
def deploy_roles(self, bundle):
|
|
tar_io = io.BytesIO(bundle)
|
|
service = self.get_authentic_service()
|
|
if not service:
|
|
return
|
|
roles_api_url = urllib.parse.urljoin(service['url'], 'api/roles/?update_or_create=slug')
|
|
provision_api_url = urllib.parse.urljoin(service['url'], 'api/provision/')
|
|
with tarfile.open(fileobj=tar_io) as tar:
|
|
manifest = json.loads(tar.extractfile('manifest.json').read().decode())
|
|
for element in manifest.get('elements'):
|
|
if element.get('type') != 'roles':
|
|
continue
|
|
role_info = json.loads(tar.extractfile('%s/%s' % (element['type'], element['slug'])).read())
|
|
# create or update
|
|
response = requests.post(roles_api_url, json=role_info)
|
|
if not response.ok:
|
|
# TODO: report failures
|
|
continue
|
|
# then force provisionning
|
|
response = requests.post(provision_api_url, json={'role_uuid': response.json()['uuid']})
|
|
if not response.ok:
|
|
# TODO: report failures
|
|
continue
|