hobo/hobo/applications/models.py

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