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
This commit is contained in:
David Moreau Simard 2018-03-16 01:25:17 -04:00
parent a1ed3a3291
commit 167fc26311
No known key found for this signature in database
GPG Key ID: 33A07694CBB71ECC
7 changed files with 421 additions and 214 deletions

View File

@ -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 from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -65,7 +65,7 @@ class Migration(migrations.Migration):
('updated', models.DateTimeField(auto_now=True)), ('updated', models.DateTimeField(auto_now=True)),
('started', models.DateTimeField(default=django.utils.timezone.now)), ('started', models.DateTimeField(default=django.utils.timezone.now)),
('ended', models.DateTimeField(blank=True, null=True)), ('ended', models.DateTimeField(blank=True, null=True)),
('name', models.TextField()), ('name', models.TextField(blank=True, null=True)),
], ],
options={ options={
'db_table': 'plays', 'db_table': 'plays',
@ -118,6 +118,8 @@ class Migration(migrations.Migration):
('unreachable', models.BooleanField(default=False)), ('unreachable', models.BooleanField(default=False)),
('ignore_errors', models.BooleanField(default=False)), ('ignore_errors', models.BooleanField(default=False)),
('result', models.BinaryField(max_length=4294967295)), ('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')), ('playbook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='api.Playbook')),
], ],
options={ options={
@ -132,24 +134,34 @@ class Migration(migrations.Migration):
('updated', models.DateTimeField(auto_now=True)), ('updated', models.DateTimeField(auto_now=True)),
('started', models.DateTimeField(default=django.utils.timezone.now)), ('started', models.DateTimeField(default=django.utils.timezone.now)),
('ended', models.DateTimeField(blank=True, null=True)), ('ended', models.DateTimeField(blank=True, null=True)),
('name', models.TextField()), ('name', models.TextField(blank=True, null=True)),
('action', models.TextField()), ('action', models.TextField()),
('lineno', models.IntegerField()), ('lineno', models.IntegerField()),
('tags', models.BinaryField(max_length=4294967295)), ('tags', models.BinaryField(max_length=4294967295)),
('handler', models.BooleanField()), ('handler', models.BooleanField()),
('file', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks', to='api.File')), ('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')), ('playbook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='api.Playbook')),
], ],
options={ options={
'db_table': 'tasks', '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( migrations.AddField(
model_name='play', model_name='play',
name='playbook', name='playbook',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plays', to='api.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( migrations.AddField(
model_name='host', model_name='host',
name='playbook', name='playbook',

View File

@ -19,45 +19,71 @@ import logging
from django.db import models from django.db import models
from django.utils import timezone 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') 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): class Base(models.Model):
"""
Abstract base model part of every model
"""
class Meta:
abstract = True
id = models.BigAutoField(primary_key=True, editable=False) id = models.BigAutoField(primary_key=True, editable=False)
created = models.DateTimeField(auto_now_add=True, editable=False) created = models.DateTimeField(auto_now_add=True, editable=False)
updated = models.DateTimeField(auto_now=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): class DurationMixin(models.Model):
started = models.DateTimeField(default=timezone.now) """
ended = models.DateTimeField(blank=True, null=True) Abstract model for models with a concept of duration
"""
@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
class Meta: class Meta:
abstract = True abstract = True
started = models.DateTimeField(default=timezone.now)
ended = models.DateTimeField(blank=True, null=True)
class Playbook(Base, DurationMixin): class Playbook(Base, DurationMixin):
""" """
@ -75,10 +101,14 @@ class Playbook(Base, DurationMixin):
def __str__(self): def __str__(self):
return '<Playbook %s:%s>' % (self.id, self.path) return '<Playbook %s:%s>' % (self.id, self.path)
__repr__ = __str__
class FileContent(Base): 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: class Meta:
db_table = 'file_contents' db_table = 'file_contents'
@ -87,10 +117,13 @@ class FileContent(Base):
def __str__(self): def __str__(self):
return '<FileContent %s:%s>' % (self.id, self.sha1) return '<FileContent %s:%s>' % (self.id, self.sha1)
__repr__ = __str__
class File(Base): class File(Base):
"""
Data about Ansible files (playbooks, tasks, role files, var files, etc).
Multiple files can reference the same FileContent record.
"""
class Meta: class Meta:
db_table = 'files' db_table = 'files'
unique_together = ('path', 'playbook',) unique_together = ('path', 'playbook',)
@ -106,10 +139,13 @@ class File(Base):
def __str__(self): def __str__(self):
return '<File %s:%s>' % (self.id, self.path) return '<File %s:%s>' % (self.id, self.path)
__repr__ = __str__
class Record(Base): 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: class Meta:
db_table = 'records' db_table = 'records'
unique_together = ('key', 'playbook',) unique_together = ('key', 'playbook',)
@ -123,10 +159,31 @@ class Record(Base):
def __str__(self): def __str__(self):
return '<Record %s:%s>' % (self.id, self.key) return '<Record %s:%s>' % (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 '<Play %s:%s>' % (self.name, self.id)
class Host(Base): class Host(Base):
"""
Data about Ansible hosts.
Contains compressed host facts and statistics about the host for the
playbook.
"""
class Meta: class Meta:
db_table = 'hosts' db_table = 'hosts'
unique_together = ('name', 'playbook',) unique_together = ('name', 'playbook',)
@ -141,35 +198,23 @@ class Host(Base):
playbook = models.ForeignKey(Playbook, playbook = models.ForeignKey(Playbook,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='hosts') related_name='hosts')
play = models.ForeignKey(Play,
on_delete=models.DO_NOTHING,
related_name='hosts')
def __str__(self): def __str__(self):
return '<Host %s:%s>' % (self.id, self.name) return '<Host %s:%s>' % (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 '<Play %s:%s>' % (self.name, self.id)
__repr__ = __str__
class Task(Base, DurationMixin): class Task(Base, DurationMixin):
"""
Data about Ansible tasks.
Results are children of Ansible tasks.
"""
class Meta: class Meta:
db_table = 'tasks' db_table = 'tasks'
name = models.TextField() name = models.TextField(blank=True, null=True)
action = models.TextField() action = models.TextField()
lineno = models.IntegerField() lineno = models.IntegerField()
tags = models.BinaryField(max_length=(2 ** 32) - 1) tags = models.BinaryField(max_length=(2 ** 32) - 1)
@ -178,48 +223,25 @@ class Task(Base, DurationMixin):
playbook = models.ForeignKey(Playbook, playbook = models.ForeignKey(Playbook,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='tasks') related_name='tasks')
play = models.ForeignKey(Play,
on_delete=models.CASCADE,
related_name='tasks')
file = models.ForeignKey(File, file = models.ForeignKey(File,
on_delete=models.DO_NOTHING, on_delete=models.DO_NOTHING,
related_name='tasks') 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): def __str__(self):
return '<Task %s:%s>' % (self.name, self.id) return '<Task %s:%s>' % (self.name, self.id)
__repr__ = __str__
class Result(Base, DurationMixin): class Result(Base, DurationMixin):
"""
Data about Ansible results.
A task can have many results if the task is run on multiple hosts.
"""
class Meta: class Meta:
db_table = 'results' 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, status = models.CharField(max_length=25,
choices=RESULT_STATUS, choices=RESULT_STATUS,
default=UNKNOWN) default=UNKNOWN)
@ -229,23 +251,19 @@ class Result(Base, DurationMixin):
unreachable = models.BooleanField(default=False) unreachable = models.BooleanField(default=False)
ignore_errors = models.BooleanField(default=False) ignore_errors = models.BooleanField(default=False)
result = models.BinaryField(max_length=(2 ** 32) - 1) result = models.BinaryField(max_length=(2 ** 32) - 1)
playbook = models.ForeignKey(Playbook, playbook = models.ForeignKey(Playbook,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='results') related_name='results')
play = models.ForeignKey(Play,
@property on_delete=models.CASCADE,
def derived_status(self): related_name='results')
if self.status == self.OK and self.changed: task = models.ForeignKey(Task,
return self.CHANGED on_delete=models.CASCADE,
elif self.status == self.FAILED and self.ignore_errors: related_name='results')
return self.IGNORED host = models.ForeignKey(Host,
elif self.status not in [ on_delete=models.CASCADE,
self.OK, self.FAILED, self.SKIPPED, self.UNREACHABLE related_name='results')
]:
return self.UNKNOWN
else:
return self.status
def __str__(self): def __str__(self):
return '<Result %s:%s>' % (self.id, self.derived_status) return '<Result %s, %s:%s>' % (self.id, self.host.name, self.status)
__repr__ = __str__

View File

@ -1,6 +1,22 @@
# -*- coding: utf-8 -*- # Copyright (c) 2018 Red Hat, Inc.
import hashlib #
# 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 <http://www.gnu.org/licenses/>.
import json import json
import hashlib
import logging import logging
import zlib import zlib
from api import models from api import models
@ -13,6 +29,10 @@ logger = logging.getLogger('api.serializers')
class CompressedTextField(serializers.CharField): 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): def to_representation(self, obj):
return zlib.decompress(obj).decode('utf8') return zlib.decompress(obj).decode('utf8')
@ -21,6 +41,11 @@ class CompressedTextField(serializers.CharField):
class CompressedObjectField(serializers.JSONField): 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): def to_representation(self, obj):
return json.loads(zlib.decompress(obj).decode('utf8')) return json.loads(zlib.decompress(obj).decode('utf8'))
@ -28,18 +53,32 @@ class CompressedObjectField(serializers.JSONField):
return zlib.compress(data.encode('utf8')) return zlib.compress(data.encode('utf8'))
class SHA1Field(serializers.CharField): class ItemDurationField(serializers.DurationField):
def to_representation(self, obj): """
return json.loads(lzma.decompress(obj).decode('utf8')) 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): def to_representation(self, obj):
return lzma.compress(data.encode('utf8')) 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): class BaseSerializer(serializers.ModelSerializer):
""" """
Serializer for the data in the model base Serializer for the data in the model base
""" """
class Meta:
abstract = True
id = serializers.IntegerField(read_only=True)
created = serializers.DateTimeField( created = serializers.DateTimeField(
read_only=True, read_only=True,
help_text='Date of creation %s' % DATE_FORMAT help_text='Date of creation %s' % DATE_FORMAT
@ -48,19 +87,15 @@ class BaseSerializer(serializers.ModelSerializer):
read_only=True, read_only=True,
help_text='Date of last update %s' % DATE_FORMAT 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): class DurationSerializer(serializers.ModelSerializer):
""" """
Serializer for duration-based fields Serializer for duration-based fields
""" """
class Meta:
abstract = True
started = serializers.DateTimeField( started = serializers.DateTimeField(
initial=timezone.now().isoformat(), initial=timezone.now().isoformat(),
help_text='Date this item started %s' % DATE_FORMAT help_text='Date this item started %s' % DATE_FORMAT
@ -69,10 +104,7 @@ class DurationSerializer(serializers.ModelSerializer):
required=False, required=False,
help_text='Date this item ended %s' % DATE_FORMAT help_text='Date this item ended %s' % DATE_FORMAT
) )
duration = serializers.DurationField( duration = ItemDurationField(source='*')
read_only=True,
help_text="Duration between 'started' and 'ended' %s" % DURATION_FORMAT
)
def validate(self, data): def validate(self, data):
""" """
@ -84,11 +116,8 @@ class DurationSerializer(serializers.ModelSerializer):
) )
return data return data
class Meta:
abstract = True
class PlaybookSerializer(serializers.HyperlinkedModelSerializer, BaseSerializer, DurationSerializer):
class PlaybookSerializer(serializers.HyperlinkedModelSerializer, BaseSerializer):
class Meta: class Meta:
model = models.Playbook model = models.Playbook
fields = '__all__' fields = '__all__'
@ -99,12 +128,13 @@ class PlaybookSerializer(serializers.HyperlinkedModelSerializer, BaseSerializer)
read_only=True, read_only=True,
help_text='Plays associated to this playbook' help_text='Plays associated to this playbook'
) )
# tasks = serializers.HyperlinkedRelatedField( tasks = serializers.HyperlinkedRelatedField(
# many=True, many=True,
# read_only=True, view_name='task-detail',
# view_name='tasks', read_only=True,
# help_text='Tasks associated to this playbook' help_text='Tasks associated to this playbook'
# ) )
# hosts = serializers.HyperlinkedRelatedField( # hosts = serializers.HyperlinkedRelatedField(
# many=True, # many=True,
# read_only=True, # read_only=True,
@ -123,17 +153,16 @@ class PlaybookSerializer(serializers.HyperlinkedModelSerializer, BaseSerializer)
# view_name='records', # view_name='records',
# help_text='Records associated to this playbook' # help_text='Records associated to this playbook'
# ) # )
# files = serializers.HyperlinkedRelatedField( files = serializers.HyperlinkedRelatedField(
# many=True, many=True,
# read_only=True, view_name='file-detail',
# view_name='files', read_only=True,
# help_text='Records associated to this playbook' help_text='Files associated to this playbook'
# ) )
# parameters = CompressedObjectField(
# parameters = CompressedObjectField( initial={},
# initial={}, help_text='A JSON dictionary containing Ansible command parameters'
# help_text='A JSON dictionary containing Ansible command parameters' )
# )
path = serializers.CharField(help_text='Path to the playbook file') path = serializers.CharField(help_text='Path to the playbook file')
ansible_version = serializers.CharField( ansible_version = serializers.CharField(
help_text='Version of Ansible used to run this playbook' 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: class Meta:
model = models.Play model = models.Play
fields = '__all__' fields = '__all__'
# playbook = serializers.HyperlinkedRelatedField(
# class TaskSerializer(BaseSerializer, DurationSerializer): view_name='playbook-detail',
# class Meta: read_only=True,
# model = models.Task help_text='Playbook associated to this play'
# fields = '__all__' )
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): # class HostSerializer(BaseSerializer):
@ -165,44 +259,65 @@ class PlaySerializer(BaseSerializer, DurationSerializer):
# class Meta: # class Meta:
# model = models.Result # model = models.Result
# fields = '__all__' # 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 RecordSerializer(BaseSerializer):
# class Meta: # class Meta:
# model = models.Record # model = models.Record
# fields = '__all__' # fields = '__all__'
# #
#
# class FileContentSerializer(BaseSerializer):
# class Meta: class FileContentSerializer(BaseSerializer):
# model = models.FileContent class Meta:
# fields = ('contents', 'sha1') 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') 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() def create(self, validated_data):
# validated_data['sha1'] = sha1 sha1 = hashlib.sha1(validated_data['contents']).hexdigest()
# obj, created = models.FileContent.objects.get_or_create( validated_data['sha1'] = sha1
# **validated_data obj, created = models.FileContent.objects.get_or_create(
# ) **validated_data
# return obj )
# return obj
#
# class FileSerializer(BaseSerializer):
# path = serializers.CharField(help_text='Path to the file') class FileSerializer(serializers.HyperlinkedModelSerializer, BaseSerializer):
# content = FileContentSerializer() class Meta:
# model = models.File
# def create(self, validated_data): fields = '__all__'
# contents = validated_data.pop('content')['contents']
# obj, created = models.FileContent.objects.get_or_create( # TODO: Why doesn't this work ? There's no playbook field shown.
# contents=contents, # Works just fine in other serializers (ex: task)
# sha1=hashlib.sha1(contents).hexdigest() # playbook = serializers.HyperlinkedRelatedField(
# ) # view_name='playbook-detail',
# validated_data['content'] = obj # read_only=True,
# return models.File.objects.create(**validated_data) # help_text='Playbook associated to this file'
# # )
# class Meta: path = serializers.CharField(help_text='Path to the file')
# model = models.File # TODO: This probably needs to be a related field to filecontent serializer
# fields = ('id', 'path', 'content', 'playbook') 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)

View File

@ -25,6 +25,10 @@ urlpatterns = [
url(r'^playbooks/(?P<pk>[0-9]+)/$', views.PlaybookDetail.as_view(), name='playbook-detail'), url(r'^playbooks/(?P<pk>[0-9]+)/$', views.PlaybookDetail.as_view(), name='playbook-detail'),
url(r'^plays/$', views.PlayList.as_view(), name='play-list'), url(r'^plays/$', views.PlayList.as_view(), name='play-list'),
url(r'^plays/(?P<pk>[0-9]+)/$', views.PlayDetail.as_view(), name='play-detail'), url(r'^plays/(?P<pk>[0-9]+)/$', views.PlayDetail.as_view(), name='play-detail'),
url(r'^tasks/$', views.TaskList.as_view(), name='task-list'),
url(r'^tasks/(?P<pk>[0-9]+)/$', views.TaskDetail.as_view(), name='task-detail'),
url(r'^files/$', views.FileList.as_view(), name='file-list'),
url(r'^files/(?P<pk>[0-9]+)/$', views.FileDetail.as_view(), name='file-detail'),
] ]
urlpatterns = format_suffix_patterns(urlpatterns) urlpatterns = format_suffix_patterns(urlpatterns)

View File

@ -27,7 +27,9 @@ from rest_framework import generics
def api_root(request, format=None): def api_root(request, format=None):
return Response({ return Response({
'playbooks': reverse('playbook-list', request=request, format=format), '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): class PlayDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = models.Play.objects.all() queryset = models.Play.objects.all()
serializer_class = serializers.PlaySerializer 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

View File

@ -17,7 +17,7 @@ SECRET_KEY = env('SECRET_KEY', preprocessor=get_secret_key, default=None)
DEBUG = env.bool('DJANGO_DEBUG', default=False) 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'),) ADMINS = (('Guillaume Vincent', 'gvincent@redhat.com'),)

View File

@ -16,9 +16,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with ARA. If not, see <http://www.gnu.org/licenses/>. # along with ARA. If not, see <http://www.gnu.org/licenses/>.
# Creates fake data in the database, bypassing the API. # Creates mock data offline leveraging the API
import django import django
import hashlib import hashlib
import json
import os import os
import sys import sys
from django.core import serializers from django.core import serializers
@ -29,40 +30,75 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ara.settings')
django.setup() django.setup()
from api import models 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( def post(endpoint, data):
started='2016-05-06T17:20:25.749489-04:00', client = Client()
name='Test play', print("Posting to %s..." % endpoint)
playbook=playbook, obj = client.post(endpoint, data)
) print("HTTP %s" % obj.status_code)
print(serializers.serialize('json', print("Got: %s" % json.dumps(obj.json(), indent=2))
models.Play.objects.all(), print("#" * 40)
indent=2))
content = 'foo'.encode('utf8') return obj
filecontent, _ = models.FileContent.objects.get_or_create(
contents=content,
sha1=hashlib.sha1(content).hexdigest()
)
print(serializers.serialize('json',
models.FileContent.objects.all(),
indent=2))
file, _ = models.File.objects.get_or_create(
playbook=playbook, playbook = post(
content=filecontent, '/api/v1/playbooks/',
path='/tmp/anothertest.yml' 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))