From 167fc2631126d89d6b0c13120b0f2abd0f8acb8b Mon Sep 17 00:00:00 2001 From: David Moreau Simard Date: Fri, 16 Mar 2018 01:25:17 -0400 Subject: [PATCH] Squashed general improvements This is a squash of several general improvements like: - Add and fix docstrings - Add new task/files endpoints - Set up file and content compression/decompression - Try to get serializers work the way we want them to Change-Id: I52ba5b31e9d225704ed271ede843f3d4a6b468b4 --- api/migrations/0001_initial.py | 20 ++- api/models.py | 208 +++++++++++++------------ api/serializers.py | 275 +++++++++++++++++++++++---------- api/urls.py | 4 + api/views.py | 24 ++- ara/settings.py | 2 +- standalone/mockdata.py | 102 ++++++++---- 7 files changed, 421 insertions(+), 214 deletions(-) diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py index 77bafa9..f277b98 100644 --- a/api/migrations/0001_initial.py +++ b/api/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.3 on 2018-03-10 17:18 +# Generated by Django 2.0.3 on 2018-03-17 19:47 from django.db import migrations, models import django.db.models.deletion @@ -65,7 +65,7 @@ class Migration(migrations.Migration): ('updated', models.DateTimeField(auto_now=True)), ('started', models.DateTimeField(default=django.utils.timezone.now)), ('ended', models.DateTimeField(blank=True, null=True)), - ('name', models.TextField()), + ('name', models.TextField(blank=True, null=True)), ], options={ 'db_table': 'plays', @@ -118,6 +118,8 @@ class Migration(migrations.Migration): ('unreachable', models.BooleanField(default=False)), ('ignore_errors', models.BooleanField(default=False)), ('result', models.BinaryField(max_length=4294967295)), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='api.Host')), + ('play', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='api.Play')), ('playbook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='api.Playbook')), ], options={ @@ -132,24 +134,34 @@ class Migration(migrations.Migration): ('updated', models.DateTimeField(auto_now=True)), ('started', models.DateTimeField(default=django.utils.timezone.now)), ('ended', models.DateTimeField(blank=True, null=True)), - ('name', models.TextField()), + ('name', models.TextField(blank=True, null=True)), ('action', models.TextField()), ('lineno', models.IntegerField()), ('tags', models.BinaryField(max_length=4294967295)), ('handler', models.BooleanField()), ('file', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks', to='api.File')), - ('play', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks', to='api.Play')), + ('play', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='api.Play')), ('playbook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='api.Playbook')), ], options={ 'db_table': 'tasks', }, ), + migrations.AddField( + model_name='result', + name='task', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='api.Task'), + ), migrations.AddField( model_name='play', name='playbook', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plays', to='api.Playbook'), ), + migrations.AddField( + model_name='host', + name='play', + field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='hosts', to='api.Play'), + ), migrations.AddField( model_name='host', name='playbook', diff --git a/api/models.py b/api/models.py index 5bbd575..2f5b129 100644 --- a/api/models.py +++ b/api/models.py @@ -19,45 +19,71 @@ import logging from django.db import models from django.utils import timezone +# Ansible statuses +OK = 'ok' +FAILED = 'failed' +SKIPPED = 'skipped' +UNREACHABLE = 'unreachable' +# ARA specific statuses (derived or assumed) +CHANGED = 'changed' +IGNORED = 'ignored' +UNKNOWN = 'unknown' + +RESULT_STATUS = ( + (OK, 'ok'), + (FAILED, 'failed'), + (SKIPPED, 'skipped'), + (UNREACHABLE, 'unreachable'), + (UNKNOWN, 'unknown') +) + logger = logging.getLogger('ara_backend.models') +# TODO: Figure out what to do when creating the first playbook file +# -> create playbook first +# -> create file/file_content and link to playbook_id (foreign key) +# -> make is_playbook = True because it's a playbook file +# -> Add a unique constraint on "is_playbook = True" for a given playbook id ? + +# TODO: Get feedback on model +# playbook -> play -> task -> host -> result +# -> host (hosts are associated/filtered to a play) +# playbook -> file <- file_content +# task -> file <- file_content +# statistics for a playbook are cumulated per host +# facts are retrieved for a host (printing those in CLI is terrible) + +# - There's multiple results for a host throughout a playbook +# - There's multiple hosts for a task +# - There's multiple tasks in a play +# - There's multiple play in a playbook +# - Hosts need to be associated to a play +# - Should all the binary things be in a single table so it's easier to shard ? +# - e.g, Reddit's ThingDB + class Base(models.Model): + """ + Abstract base model part of every model + """ + class Meta: + abstract = True + id = models.BigAutoField(primary_key=True, editable=False) created = models.DateTimeField(auto_now_add=True, editable=False) updated = models.DateTimeField(auto_now=True, editable=False) - @property - def age(self): - """ - Calculates duration between created and updated. - """ - return self.updated - self.created - - class Meta: - abstract = True - class DurationMixin(models.Model): - started = models.DateTimeField(default=timezone.now) - ended = models.DateTimeField(blank=True, null=True) - - @property - def duration(self): - """ - Calculates duration between started and ended or between started and - updated if we do not yet have an end. - """ - if self.ended is None: - if self.started is None: - return timezone.timedelta(seconds=0) - else: - return self.updated - self.started - return self.ended - self.started - + """ + Abstract model for models with a concept of duration + """ class Meta: abstract = True + started = models.DateTimeField(default=timezone.now) + ended = models.DateTimeField(blank=True, null=True) + class Playbook(Base, DurationMixin): """ @@ -75,10 +101,14 @@ class Playbook(Base, DurationMixin): def __str__(self): return '' % (self.id, self.path) - __repr__ = __str__ class FileContent(Base): + """ + Contents of a uniquely stored and compressed file. + Running the same playbook twice will yield two playbook files but just + one file contents. + """ class Meta: db_table = 'file_contents' @@ -87,10 +117,13 @@ class FileContent(Base): def __str__(self): return '' % (self.id, self.sha1) - __repr__ = __str__ class File(Base): + """ + Data about Ansible files (playbooks, tasks, role files, var files, etc). + Multiple files can reference the same FileContent record. + """ class Meta: db_table = 'files' unique_together = ('path', 'playbook',) @@ -106,10 +139,13 @@ class File(Base): def __str__(self): return '' % (self.id, self.path) - __repr__ = __str__ class Record(Base): + """ + A rudimentary key/value table to associate arbitrary data to a playbook. + Used with the ara_record and ara_read Ansible modules. + """ class Meta: db_table = 'records' unique_together = ('key', 'playbook',) @@ -123,10 +159,31 @@ class Record(Base): def __str__(self): return '' % (self.id, self.key) - __repr__ = __str__ + + +class Play(Base, DurationMixin): + """ + Data about Ansible plays. + Hosts, tasks and results are childrens of an Ansible play. + """ + class Meta: + db_table = 'plays' + + name = models.TextField(blank=True, null=True) + playbook = models.ForeignKey(Playbook, + on_delete=models.CASCADE, + related_name='plays') + + def __str__(self): + return '' % (self.name, self.id) class Host(Base): + """ + Data about Ansible hosts. + Contains compressed host facts and statistics about the host for the + playbook. + """ class Meta: db_table = 'hosts' unique_together = ('name', 'playbook',) @@ -141,35 +198,23 @@ class Host(Base): playbook = models.ForeignKey(Playbook, on_delete=models.CASCADE, related_name='hosts') + play = models.ForeignKey(Play, + on_delete=models.DO_NOTHING, + related_name='hosts') def __str__(self): return '' % (self.id, self.name) - __repr__ = __str__ - - -class Play(Base, DurationMixin): - class Meta: - db_table = 'plays' - - name = models.TextField() - playbook = models.ForeignKey(Playbook, - on_delete=models.CASCADE, - related_name='plays') - - @property - def offset_from_playbook(self): - return self.started - self.playbook.started - - def __str__(self): - return '' % (self.name, self.id) - __repr__ = __str__ class Task(Base, DurationMixin): + """ + Data about Ansible tasks. + Results are children of Ansible tasks. + """ class Meta: db_table = 'tasks' - name = models.TextField() + name = models.TextField(blank=True, null=True) action = models.TextField() lineno = models.IntegerField() tags = models.BinaryField(max_length=(2 ** 32) - 1) @@ -178,48 +223,25 @@ class Task(Base, DurationMixin): playbook = models.ForeignKey(Playbook, on_delete=models.CASCADE, related_name='tasks') + play = models.ForeignKey(Play, + on_delete=models.CASCADE, + related_name='tasks') file = models.ForeignKey(File, on_delete=models.DO_NOTHING, related_name='tasks') - play = models.ForeignKey(Play, - on_delete=models.DO_NOTHING, - related_name='tasks') - - @property - def offset_from_playbook(self): - return self.started - self.playbook.started - - @property - def offset_from_play(self): - return self.started - self.play.started def __str__(self): return '' % (self.name, self.id) - __repr__ = __str__ class Result(Base, DurationMixin): + """ + Data about Ansible results. + A task can have many results if the task is run on multiple hosts. + """ class Meta: db_table = 'results' - # Ansible statuses - OK = 'ok' - FAILED = 'failed' - SKIPPED = 'skipped' - UNREACHABLE = 'unreachable' - # ARA specific statuses (derived or assumed) - CHANGED = 'changed' - IGNORED = 'ignored' - UNKNOWN = 'unknown' - - RESULT_STATUS = ( - (OK, 'ok'), - (FAILED, 'failed'), - (SKIPPED, 'skipped'), - (UNREACHABLE, 'unreachable'), - (UNKNOWN, 'unknown') - ) - status = models.CharField(max_length=25, choices=RESULT_STATUS, default=UNKNOWN) @@ -229,23 +251,19 @@ class Result(Base, DurationMixin): unreachable = models.BooleanField(default=False) ignore_errors = models.BooleanField(default=False) result = models.BinaryField(max_length=(2 ** 32) - 1) + playbook = models.ForeignKey(Playbook, on_delete=models.CASCADE, related_name='results') - - @property - def derived_status(self): - if self.status == self.OK and self.changed: - return self.CHANGED - elif self.status == self.FAILED and self.ignore_errors: - return self.IGNORED - elif self.status not in [ - self.OK, self.FAILED, self.SKIPPED, self.UNREACHABLE - ]: - return self.UNKNOWN - else: - return self.status + play = models.ForeignKey(Play, + on_delete=models.CASCADE, + related_name='results') + task = models.ForeignKey(Task, + on_delete=models.CASCADE, + related_name='results') + host = models.ForeignKey(Host, + on_delete=models.CASCADE, + related_name='results') def __str__(self): - return '' % (self.id, self.derived_status) - __repr__ = __str__ + return '' % (self.id, self.host.name, self.status) diff --git a/api/serializers.py b/api/serializers.py index d371a4f..d550682 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,6 +1,22 @@ -# -*- coding: utf-8 -*- -import hashlib +# Copyright (c) 2018 Red Hat, Inc. +# +# This file is part of ARA Records Ansible. +# +# ARA is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ARA 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with ARA. If not, see . + import json +import hashlib import logging import zlib from api import models @@ -13,6 +29,10 @@ logger = logging.getLogger('api.serializers') class CompressedTextField(serializers.CharField): + """ + Compresses text before storing it in the database. + Decompresses text from the database before serving it. + """ def to_representation(self, obj): return zlib.decompress(obj).decode('utf8') @@ -21,6 +41,11 @@ class CompressedTextField(serializers.CharField): class CompressedObjectField(serializers.JSONField): + """ + Serializes/compresses an object (i.e, list, dict) before storing it in the + database. + Decompresses/deserializes an object before serving it. + """ def to_representation(self, obj): return json.loads(zlib.decompress(obj).decode('utf8')) @@ -28,18 +53,32 @@ class CompressedObjectField(serializers.JSONField): return zlib.compress(data.encode('utf8')) -class SHA1Field(serializers.CharField): - def to_representation(self, obj): - return json.loads(lzma.decompress(obj).decode('utf8')) +class ItemDurationField(serializers.DurationField): + """ + Calculates duration between started and ended or between started and + updated if we do not yet have an end. + """ + def __init__(self, **kwargs): + kwargs['read_only'] = True + super(ItemDurationField, self).__init__(**kwargs) - def to_internal_value(self, data): - return lzma.compress(data.encode('utf8')) + def to_representation(self, obj): + if obj.ended is None: + if obj.started is None: + return timezone.timedelta(seconds=0) + else: + return obj.updated - obj.started + return obj.ended - obj.started class BaseSerializer(serializers.ModelSerializer): """ Serializer for the data in the model base """ + class Meta: + abstract = True + + id = serializers.IntegerField(read_only=True) created = serializers.DateTimeField( read_only=True, help_text='Date of creation %s' % DATE_FORMAT @@ -48,19 +87,15 @@ class BaseSerializer(serializers.ModelSerializer): read_only=True, help_text='Date of last update %s' % DATE_FORMAT ) - age = serializers.DurationField( - read_only=True, - help_text='Duration since the creation %s' % DURATION_FORMAT - ) - - class Meta: - abstract = True class DurationSerializer(serializers.ModelSerializer): """ Serializer for duration-based fields """ + class Meta: + abstract = True + started = serializers.DateTimeField( initial=timezone.now().isoformat(), help_text='Date this item started %s' % DATE_FORMAT @@ -69,10 +104,7 @@ class DurationSerializer(serializers.ModelSerializer): required=False, help_text='Date this item ended %s' % DATE_FORMAT ) - duration = serializers.DurationField( - read_only=True, - help_text="Duration between 'started' and 'ended' %s" % DURATION_FORMAT - ) + duration = ItemDurationField(source='*') def validate(self, data): """ @@ -84,11 +116,8 @@ class DurationSerializer(serializers.ModelSerializer): ) return data - class Meta: - abstract = True - -class PlaybookSerializer(serializers.HyperlinkedModelSerializer, BaseSerializer): +class PlaybookSerializer(serializers.HyperlinkedModelSerializer, BaseSerializer, DurationSerializer): class Meta: model = models.Playbook fields = '__all__' @@ -99,12 +128,13 @@ class PlaybookSerializer(serializers.HyperlinkedModelSerializer, BaseSerializer) read_only=True, help_text='Plays associated to this playbook' ) -# tasks = serializers.HyperlinkedRelatedField( -# many=True, -# read_only=True, -# view_name='tasks', -# help_text='Tasks associated to this playbook' -# ) + tasks = serializers.HyperlinkedRelatedField( + many=True, + view_name='task-detail', + read_only=True, + help_text='Tasks associated to this playbook' + ) + # hosts = serializers.HyperlinkedRelatedField( # many=True, # read_only=True, @@ -123,17 +153,16 @@ class PlaybookSerializer(serializers.HyperlinkedModelSerializer, BaseSerializer) # view_name='records', # help_text='Records associated to this playbook' # ) -# files = serializers.HyperlinkedRelatedField( -# many=True, -# read_only=True, -# view_name='files', -# help_text='Records associated to this playbook' -# ) -# -# parameters = CompressedObjectField( -# initial={}, -# help_text='A JSON dictionary containing Ansible command parameters' -# ) + files = serializers.HyperlinkedRelatedField( + many=True, + view_name='file-detail', + read_only=True, + help_text='Files associated to this playbook' + ) + parameters = CompressedObjectField( + initial={}, + help_text='A JSON dictionary containing Ansible command parameters' + ) path = serializers.CharField(help_text='Path to the playbook file') ansible_version = serializers.CharField( help_text='Version of Ansible used to run this playbook' @@ -143,16 +172,81 @@ class PlaybookSerializer(serializers.HyperlinkedModelSerializer, BaseSerializer) ) -class PlaySerializer(BaseSerializer, DurationSerializer): +class PlaySerializer(serializers.HyperlinkedModelSerializer, BaseSerializer, DurationSerializer): class Meta: model = models.Play fields = '__all__' -# -# class TaskSerializer(BaseSerializer, DurationSerializer): -# class Meta: -# model = models.Task -# fields = '__all__' + playbook = serializers.HyperlinkedRelatedField( + view_name='playbook-detail', + read_only=True, + help_text='Playbook associated to this play' + ) + tasks = serializers.HyperlinkedRelatedField( + many=True, + view_name='task-detail', + read_only=True, + help_text='Tasks associated to this play' + ) + name = serializers.CharField( + help_text='Name of the play', + allow_blank=True, + allow_null=True, + ) + # hosts = serializers.HyperlinkedRelatedField( + # many=True, + # view_name='host-detail', + # read_only=True, + # help_text='Hosts associated to this play' + #) + + +class TaskSerializer(serializers.HyperlinkedModelSerializer, BaseSerializer, DurationSerializer): + class Meta: + model = models.Task + fields = '__all__' + + playbook = serializers.HyperlinkedRelatedField( + view_name='playbook-detail', + read_only=True, + help_text='Playbook associated to this task' + ) + play = serializers.HyperlinkedRelatedField( + view_name='play-detail', + read_only=True, + help_text='Play associated to this task' + ) + file = serializers.HyperlinkedRelatedField( + view_name='file-detail', + read_only=True, + help_text='File associated to this task' + ) + # results = serializers.HyperlinkedRelatedField( + # many=True, + # view_name='result-detail', + # read_only=True, + # help_text='Results associated to this task' + # ) + name = serializers.CharField( + help_text='Name of the task', + allow_blank=True, + allow_null=True + ) + action = serializers.CharField(help_text='Action of the task') + lineno = serializers.IntegerField( + help_text='Line number in the file of the task' + ) + tags = CompressedObjectField( + help_text='A JSON list containing Ansible tags', + initial=[], + default=[], + ) + handler = serializers.BooleanField( + help_text='Whether or not this task was a handler', + initial=False, + default=False, + ) + # # # class HostSerializer(BaseSerializer): @@ -165,44 +259,65 @@ class PlaySerializer(BaseSerializer, DurationSerializer): # class Meta: # model = models.Result # fields = '__all__' -# +# @property +# def derived_status(self): +# if self.status == self.OK and self.changed: +# return self.CHANGED +# elif self.status == self.FAILED and self.ignore_errors: +# return self.IGNORED +# elif self.status not in [ +# self.OK, self.FAILED, self.SKIPPED, self.UNREACHABLE +# ]: +# return self.UNKNOWN +# else: +# return self.status # # class RecordSerializer(BaseSerializer): # class Meta: # model = models.Record # fields = '__all__' # -# -# class FileContentSerializer(BaseSerializer): -# class Meta: -# model = models.FileContent -# fields = ('contents', 'sha1') -# -# contents = CompressedTextField(help_text='Contents of the file') -# sha1 = serializers.CharField(read_only=True, help_text='sha1 of the file') -# -# def create(self, validated_data): -# sha1 = hashlib.sha1(validated_data['contents']).hexdigest() -# validated_data['sha1'] = sha1 -# obj, created = models.FileContent.objects.get_or_create( -# **validated_data -# ) -# return obj -# -# -# class FileSerializer(BaseSerializer): -# path = serializers.CharField(help_text='Path to the file') -# content = FileContentSerializer() -# -# def create(self, validated_data): -# contents = validated_data.pop('content')['contents'] -# obj, created = models.FileContent.objects.get_or_create( -# contents=contents, -# sha1=hashlib.sha1(contents).hexdigest() -# ) -# validated_data['content'] = obj -# return models.File.objects.create(**validated_data) -# -# class Meta: -# model = models.File -# fields = ('id', 'path', 'content', 'playbook') + + +class FileContentSerializer(BaseSerializer): + class Meta: + model = models.FileContent + fields = '__all__' + + contents = CompressedTextField(help_text='Contents of the file') + sha1 = serializers.CharField(read_only=True, help_text='sha1 of the file') + + def create(self, validated_data): + sha1 = hashlib.sha1(validated_data['contents']).hexdigest() + validated_data['sha1'] = sha1 + obj, created = models.FileContent.objects.get_or_create( + **validated_data + ) + return obj + + +class FileSerializer(serializers.HyperlinkedModelSerializer, BaseSerializer): + class Meta: + model = models.File + fields = '__all__' + + # TODO: Why doesn't this work ? There's no playbook field shown. + # Works just fine in other serializers (ex: task) + # playbook = serializers.HyperlinkedRelatedField( + # view_name='playbook-detail', + # read_only=True, + # help_text='Playbook associated to this file' + # ) + path = serializers.CharField(help_text='Path to the file') + # TODO: This probably needs to be a related field to filecontent serializer + content = serializers.CharField() + is_playbook = serializers.BooleanField(default=False) + + def create(self, validated_data): + content = validated_data.pop('content') + obj, created = models.FileContent.objects.get_or_create( + contents=content.encode('utf8'), + sha1=hashlib.sha1(content.encode('utf8')).hexdigest() + ) + validated_data['content'] = obj + return models.File.objects.create(**validated_data) diff --git a/api/urls.py b/api/urls.py index 8ac8e11..2dcde7d 100644 --- a/api/urls.py +++ b/api/urls.py @@ -25,6 +25,10 @@ urlpatterns = [ url(r'^playbooks/(?P[0-9]+)/$', views.PlaybookDetail.as_view(), name='playbook-detail'), url(r'^plays/$', views.PlayList.as_view(), name='play-list'), url(r'^plays/(?P[0-9]+)/$', views.PlayDetail.as_view(), name='play-detail'), + url(r'^tasks/$', views.TaskList.as_view(), name='task-list'), + url(r'^tasks/(?P[0-9]+)/$', views.TaskDetail.as_view(), name='task-detail'), + url(r'^files/$', views.FileList.as_view(), name='file-list'), + url(r'^files/(?P[0-9]+)/$', views.FileDetail.as_view(), name='file-detail'), ] urlpatterns = format_suffix_patterns(urlpatterns) diff --git a/api/views.py b/api/views.py index 1226366..7d28580 100644 --- a/api/views.py +++ b/api/views.py @@ -27,7 +27,9 @@ from rest_framework import generics def api_root(request, format=None): return Response({ 'playbooks': reverse('playbook-list', request=request, format=format), - 'plays': reverse('play-list', request=request, format=format) + 'plays': reverse('play-list', request=request, format=format), + 'tasks': reverse('task-list', request=request, format=format), + 'files': reverse('file-list', request=request, format=format) }) @@ -49,3 +51,23 @@ class PlayList(generics.ListCreateAPIView): class PlayDetail(generics.RetrieveUpdateDestroyAPIView): queryset = models.Play.objects.all() serializer_class = serializers.PlaySerializer + + +class TaskList(generics.ListCreateAPIView): + queryset = models.Task.objects.all() + serializer_class = serializers.TaskSerializer + + +class TaskDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = models.Task.objects.all() + serializer_class = serializers.TaskSerializer + + +class FileList(generics.ListCreateAPIView): + queryset = models.File.objects.all() + serializer_class = serializers.FileSerializer + + +class FileDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = models.File.objects.all() + serializer_class = serializers.FileSerializer diff --git a/ara/settings.py b/ara/settings.py index 28669ed..6f72d26 100644 --- a/ara/settings.py +++ b/ara/settings.py @@ -17,7 +17,7 @@ SECRET_KEY = env('SECRET_KEY', preprocessor=get_secret_key, default=None) DEBUG = env.bool('DJANGO_DEBUG', default=False) -ALLOWED_HOSTS = env('ALLOWED_HOSTS', cast=list, default=['localhost', '127.0.0.1']) +ALLOWED_HOSTS = env('ALLOWED_HOSTS', cast=list, default=['localhost', '127.0.0.1', 'testserver']) ADMINS = (('Guillaume Vincent', 'gvincent@redhat.com'),) diff --git a/standalone/mockdata.py b/standalone/mockdata.py index ec4183a..3763fda 100644 --- a/standalone/mockdata.py +++ b/standalone/mockdata.py @@ -16,9 +16,10 @@ # You should have received a copy of the GNU General Public License # along with ARA. If not, see . -# Creates fake data in the database, bypassing the API. +# Creates mock data offline leveraging the API import django import hashlib +import json import os import sys from django.core import serializers @@ -29,40 +30,75 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ara.settings') django.setup() from api import models +from django.test import Client -playbook, _ = models.Playbook.objects.get_or_create( - started='2016-05-06T17:20:25.749489-04:00', - path='/tmp/test.yml', - ansible_version='2.3.4', - completed=False, -) -print(serializers.serialize('json', - models.Playbook.objects.all(), - indent=2)) -play, _ = models.Play.objects.get_or_create( - started='2016-05-06T17:20:25.749489-04:00', - name='Test play', - playbook=playbook, -) -print(serializers.serialize('json', - models.Play.objects.all(), - indent=2)) +def post(endpoint, data): + client = Client() + print("Posting to %s..." % endpoint) + obj = client.post(endpoint, data) + print("HTTP %s" % obj.status_code) + print("Got: %s" % json.dumps(obj.json(), indent=2)) + print("#" * 40) -content = 'foo'.encode('utf8') -filecontent, _ = models.FileContent.objects.get_or_create( - contents=content, - sha1=hashlib.sha1(content).hexdigest() -) -print(serializers.serialize('json', - models.FileContent.objects.all(), - indent=2)) + return obj -file, _ = models.File.objects.get_or_create( - playbook=playbook, - content=filecontent, - path='/tmp/anothertest.yml' + +playbook = post( + '/api/v1/playbooks/', + dict( + started='2016-05-06T17:20:25.749489-04:00', + path='/tmp/playbook.yml', + ansible_version='2.3.4', + completed=False, + parameters=json.dumps(dict( + foo='bar' + )) + ) +) + +play = post( + '/api/v1/plays/', + dict( + started='2016-05-06T17:20:25.749489-04:00', + name='Test play', + playbook=playbook.json()['url'] + ) +) + +playbook_file = post( + '/api/v1/files/', + dict( + path=playbook.json()['path'], + # TODO: Fix this somehow + content='# playbook', + playbook=playbook.json()['url'], + is_playbook=True + ) +) + +task_file = post( + '/api/v1/files/', + dict( + playbook=playbook.json()['url'], + path='/tmp/task.yml', + # TODO: Fix this somehow + content='# task', + is_playbook=True + ) +) + +task = post( + '/api/v1/tasks/', + dict( + playbook=playbook.json()['url'], + play=play.json()['url'], + file=task_file.json()['url'], + name='Task name', + action='action', + lineno=1, + tags=json.dumps(['one', 'two']), + handler=False, + started='2016-05-06T17:20:25.749489-04:00' + ) ) -print(serializers.serialize('json', - models.File.objects.all(), - indent=2))