Refactor django models

- Turns out that some datetime fields should actually use timezone.now
- Move file/filecontent out of a relationship with playbooks
- Add many/many relationships for files/filecontents
- Add some tests

Change-Id: I2e83c0a584b49069e423a9ec8c2c9025a52ea7ef
This commit is contained in:
Guillaume Vincent 2018-03-20 12:16:58 +01:00 committed by David Moreau Simard
parent 69cc8dcac1
commit 17f6c9eca5
No known key found for this signature in database
GPG Key ID: 33A07694CBB71ECC
9 changed files with 312 additions and 353 deletions

View File

@ -1,7 +1,8 @@
# Generated by Django 2.0.3 on 2018-03-19 11:38
# Generated by Django 2.0.3 on 2018-03-20 13:55
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
@ -19,7 +20,6 @@ class Migration(migrations.Migration):
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('path', models.CharField(max_length=255)),
('is_playbook', models.BooleanField(default=False)),
],
options={
'db_table': 'files',
@ -62,9 +62,9 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(editable=False, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('started', models.DateTimeField(auto_now_add=True)),
('started', models.DateTimeField(default=django.utils.timezone.now)),
('ended', models.DateTimeField(blank=True, null=True)),
('name', models.TextField(blank=True, null=True)),
('name', models.CharField(blank=True, max_length=255, null=True)),
],
options={
'db_table': 'plays',
@ -76,12 +76,12 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(editable=False, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('started', models.DateTimeField(auto_now_add=True)),
('started', models.DateTimeField(default=django.utils.timezone.now)),
('ended', models.DateTimeField(blank=True, null=True)),
('path', models.CharField(max_length=255)),
('ansible_version', models.CharField(max_length=255)),
('parameters', models.BinaryField(max_length=4294967295)),
('completed', models.BooleanField(default=False)),
('parameters', models.BinaryField(max_length=4294967295)),
('files', models.ManyToManyField(to='api.File')),
],
options={
'db_table': 'playbooks',
@ -108,18 +108,10 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(editable=False, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('started', models.DateTimeField(auto_now_add=True)),
('started', models.DateTimeField(default=django.utils.timezone.now)),
('ended', models.DateTimeField(blank=True, null=True)),
('status', models.CharField(choices=[('ok', 'ok'), ('failed', 'failed'), ('skipped', 'skipped'), ('unreachable', 'unreachable'), ('unknown', 'unknown')], default='unknown', max_length=25)),
('changed', models.BooleanField(default=False)),
('failed', models.BooleanField(default=False)),
('skipped', models.BooleanField(default=False)),
('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')),
('status', models.CharField(choices=[('ok', 'ok'), ('failed', 'failed'), ('skipped', 'skipped'), ('unreachable', 'unreachable'), ('changed', 'changed'), ('ignored', 'ignored'), ('unknown', 'unknown')], default='unknown', max_length=25)),
('content', models.BinaryField(max_length=4294967295)),
],
options={
'db_table': 'results',
@ -131,31 +123,36 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(editable=False, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('started', models.DateTimeField(auto_now_add=True)),
('started', models.DateTimeField(default=django.utils.timezone.now)),
('ended', models.DateTimeField(blank=True, null=True)),
('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')),
('files', models.ManyToManyField(to='api.File')),
('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')),
('results', models.ManyToManyField(to='api.Result')),
],
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'),
model_name='playbook',
name='results',
field=models.ManyToManyField(to='api.Result'),
),
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='play',
name='results',
field=models.ManyToManyField(to='api.Result'),
),
migrations.AddField(
model_name='host',
name='play',
@ -163,29 +160,20 @@ class Migration(migrations.Migration):
),
migrations.AddField(
model_name='host',
name='playbook',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hosts', to='api.Playbook'),
name='results',
field=models.ManyToManyField(to='api.Result'),
),
migrations.AddField(
model_name='file',
name='content',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='api.FileContent'),
),
migrations.AddField(
model_name='file',
name='playbook',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='api.Playbook'),
),
migrations.AlterUniqueTogether(
name='record',
unique_together={('key', 'playbook')},
),
migrations.AlterUniqueTogether(
name='host',
unique_together={('name', 'playbook')},
),
migrations.AlterUniqueTogether(
name='file',
unique_together={('path', 'playbook')},
unique_together={('name', 'play')},
),
]

View File

@ -15,69 +15,28 @@
# You should have received a copy of the GNU General Public License
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
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
# note: GV better is id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
id = models.BigAutoField(primary_key=True, editable=False)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class DurationMixin(models.Model):
class Duration(Base):
"""
Abstract model for models with a concept of duration
"""
class Meta:
abstract = True
@ -85,30 +44,13 @@ class DurationMixin(models.Model):
ended = models.DateTimeField(blank=True, null=True)
class Playbook(Base, DurationMixin):
"""
The 'playbook' table represents a single execution of the ansible or
ansible-playbook commands. All the data for that execution is tied back
to this one playbook.
"""
class Meta:
db_table = 'playbooks'
path = models.CharField(max_length=255)
ansible_version = models.CharField(max_length=255)
parameters = models.BinaryField(max_length=(2 ** 32) - 1)
completed = models.BooleanField(default=False)
def __str__(self):
return '<Playbook %s:%s>' % (self.id, self.path)
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'
@ -124,28 +66,79 @@ 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',)
path = models.CharField(max_length=255)
is_playbook = models.BooleanField(default=False)
content = models.ForeignKey(FileContent,
on_delete=models.CASCADE,
related_name='files')
playbook = models.ForeignKey(Playbook,
on_delete=models.CASCADE,
related_name='files')
content = models.ForeignKey(FileContent, on_delete=models.CASCADE, related_name='files')
def __str__(self):
return '<File %s:%s>' % (self.id, self.path)
class Result(Duration):
"""
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'
STATUS = (
(OK, 'ok'),
(FAILED, 'failed'),
(SKIPPED, 'skipped'),
(UNREACHABLE, 'unreachable'),
(CHANGED, 'changed'),
(IGNORED, 'ignored'),
(UNKNOWN, 'unknown')
)
status = models.CharField(max_length=25, choices=STATUS, default=UNKNOWN)
content = models.BinaryField(max_length=(2 ** 32) - 1)
def __str__(self):
return '<Result %s, %s>' % (self.id, self.status)
class Playbook(Duration):
"""
An entry in the 'playbooks' table represents a single execution of the
ansible or ansible-playbook commands. All the data for that execution
is tied back to this one playbook.
"""
class Meta:
db_table = 'playbooks'
ansible_version = models.CharField(max_length=255)
completed = models.BooleanField(default=False)
parameters = models.BinaryField(max_length=(2 ** 32) - 1)
files = models.ManyToManyField(File)
results = models.ManyToManyField(Result)
def __str__(self):
return '<Playbook %s>' % self.id
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',)
@ -153,64 +146,32 @@ class Record(Base):
key = models.CharField(max_length=255)
value = models.TextField(null=True, blank=True)
type = models.CharField(max_length=255)
playbook = models.ForeignKey(Playbook,
on_delete=models.CASCADE,
related_name='records')
playbook = models.ForeignKey(Playbook, on_delete=models.CASCADE, related_name='records')
def __str__(self):
return '<Record %s:%s>' % (self.id, self.key)
class Play(Base, DurationMixin):
class Play(Duration):
"""
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')
name = models.CharField(max_length=255, blank=True, null=True)
playbook = models.ForeignKey(Playbook, on_delete=models.CASCADE, related_name='plays')
results = models.ManyToManyField(Result)
def __str__(self):
return '<Play %s:%s>' % (self.name, self.id)
return '<Play %s:%s>' % (self.id, self.name)
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',)
class Task(Duration):
"""Data about Ansible tasks."""
name = models.CharField(max_length=255)
facts = models.BinaryField(max_length=(2 ** 32) - 1)
changed = models.IntegerField(default=0)
failed = models.IntegerField(default=0)
ok = models.IntegerField(default=0)
skipped = models.IntegerField(default=0)
unreachable = models.IntegerField(default=0)
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 '<Host %s:%s>' % (self.id, self.name)
class Task(Base, DurationMixin):
"""
Data about Ansible tasks.
Results are children of Ansible tasks.
"""
class Meta:
db_table = 'tasks'
@ -220,50 +181,34 @@ class Task(Base, DurationMixin):
tags = models.BinaryField(max_length=(2 ** 32) - 1)
handler = models.BooleanField()
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.CASCADE, related_name='tasks')
results = models.ManyToManyField(Result)
files = models.ManyToManyField(File)
def __str__(self):
return '<Task %s:%s>' % (self.name, self.id)
class Result(Base, DurationMixin):
class Host(Base):
"""
Data about Ansible results.
A task can have many results if the task is run on multiple hosts.
Data about Ansible hosts.
Contains compressed host facts and statistics about the host for the
playbook.
"""
class Meta:
db_table = 'results'
db_table = 'hosts'
unique_together = ('name', 'play',)
status = models.CharField(max_length=25,
choices=RESULT_STATUS,
default=UNKNOWN)
changed = models.BooleanField(default=False)
failed = models.BooleanField(default=False)
skipped = models.BooleanField(default=False)
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')
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')
name = models.CharField(max_length=255)
facts = models.BinaryField(max_length=(2 ** 32) - 1)
changed = models.IntegerField(default=0)
failed = models.IntegerField(default=0)
ok = models.IntegerField(default=0)
skipped = models.IntegerField(default=0)
unreachable = models.IntegerField(default=0)
play = models.ForeignKey(Play, on_delete=models.DO_NOTHING, related_name='hosts')
results = models.ManyToManyField(Result)
def __str__(self):
return '<Result %s, %s:%s>' % (self.id, self.host.name, self.status)
return '<Host %s:%s>' % (self.id, self.name)

View File

@ -55,44 +55,6 @@ class CompressedObjectField(serializers.JSONField):
return zlib.compress(json.dumps(data).encode('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_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
)
updated = serializers.DateTimeField(
read_only=True,
help_text='Date of last update %s' % DATE_FORMAT
)
class DurationSerializer(serializers.ModelSerializer):
"""
Serializer for duration-based fields
@ -110,29 +72,67 @@ class DurationSerializer(serializers.ModelSerializer):
return obj.ended - obj.started
class FileContentSerializer(serializers.ModelSerializer):
class Meta:
model = models.FileContent
fields = '__all__'
class FileContentField(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.contents).decode('utf8')
def to_internal_value(self, data):
contents = zlib.compress(data.encode('utf8'))
sha1 = hashlib.sha1(contents).hexdigest()
content_file, created = models.FileContent.objects.get_or_create(sha1=sha1, defaults={
'sha1': sha1,
'contents': contents
})
return content_file
class FileSerializer(serializers.ModelSerializer):
class Meta:
model = models.File
fields = '__all__'
content = FileContentField()
class ResultSerializer(serializers.ModelSerializer):
class Meta:
model = models.Result
fields = '__all__'
class PlaybookSerializer(DurationSerializer):
class Meta:
model = models.Playbook
fields = '__all__'
path = serializers.CharField(help_text='Path to the playbook file')
ansible_version = serializers.CharField(
help_text='Version of Ansible used to run this playbook'
)
parameters = CompressedObjectField(
default=zlib.compress(json.dumps({}).encode('utf8')),
help_text='A JSON dictionary containing Ansible command parameters'
)
completed = serializers.BooleanField(
default=False,
help_text='If the completion of the execution has been acknowledged'
)
plays = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
tasks = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
files = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
hosts = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
results = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
records = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
files = FileSerializer(many=True, default=[])
results = ResultSerializer(read_only=True, many=True)
def create(self, validated_data):
files = validated_data.pop('files')
playbook = models.Playbook.objects.create(**validated_data)
for file in files:
playbook.files.add(models.File.objects.create(**file))
return playbook
def update(self, instance, validated_data):
files = validated_data.pop('files')
return super(PlaybookSerializer, self).update(instance, validated_data)
class PlaySerializer(DurationSerializer):
@ -140,6 +140,8 @@ class PlaySerializer(DurationSerializer):
model = models.Play
fields = '__all__'
results = ResultSerializer(read_only=True, many=True)
class TaskSerializer(DurationSerializer):
class Meta:
@ -150,44 +152,3 @@ class TaskSerializer(DurationSerializer):
default=zlib.compress(json.dumps([]).encode('utf8')),
help_text='A JSON list containing Ansible tags'
)
class FileContentSerializer(BaseSerializer):
class Meta:
model = models.FileContent
fields = '__all__'
sha1 = serializers.CharField(read_only=True, help_text='sha1 of the file')
contents = CompressedTextField(help_text='Contents of the file')
def create(self, validated_data):
sha1 = hashlib.sha1(validated_data['contents']).hexdigest()
validated_data['sha1'] = sha1
return models.FileContent.objects.create(**validated_data)
class FileSerializer(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)

View File

@ -7,14 +7,23 @@ class PlaybookFactory(factory.DjangoModelFactory):
class Meta:
model = models.Playbook
path = '/tmp/playbook.yml'
ansible_version = '2.4.0'
completed = True
parameters = b'x\x9c\xabVJ\xcb\xcfW\xb2RPJJ,R\xaa\x05\x00 \x98\x04T'
class FileContentFactory(factory.DjangoModelFactory):
class Meta:
model = models.FileContent
django_get_or_create = ('sha1',)
sha1 = '1e58ead094c920fad631d2c22df34dc0314dab0c'
contents = b'x\x9cSV(\xc8I\xacL\xca\xcf\xcf\x06\x00\x11\xbd\x03\xa5'
class FileFactory(factory.DjangoModelFactory):
class Meta:
model = models.File
path = '/tmp/playbook.yml'
content = factory.SubFactory(FileContentFactory)

85
api/tests/tests_file.py Normal file
View File

@ -0,0 +1,85 @@
from rest_framework.test import APITestCase
from api import models, serializers
from api.tests import factories
class FileTestCase(APITestCase):
def test_file_factory(self):
file_content = factories.FileContentFactory()
file = factories.FileFactory(path='/tmp/playbook.yml', content=file_content)
self.assertEqual(file.path, '/tmp/playbook.yml')
self.assertEqual(file.content.sha1, file_content.sha1)
def test_file_serializer(self):
serializer = serializers.FileSerializer(data={
'path': '/tmp/playbook.yml',
'content': '# playbook'
})
serializer.is_valid()
file = serializer.save()
file.refresh_from_db()
self.assertEqual(file.content.sha1, '1e58ead094c920fad631d2c22df34dc0314dab0c')
def test_create_file_with_same_content_create_only_one_file_content(self):
content = '# playbook'
serializer = serializers.FileSerializer(data={
'path': '/tmp/1/playbook.yml',
'content': content
})
serializer.is_valid()
file_content = serializer.save()
file_content.refresh_from_db()
serializer2 = serializers.FileSerializer(data={
'path': '/tmp/2/playbook.yml',
'content': content
})
serializer2.is_valid()
file_content = serializer2.save()
file_content.refresh_from_db()
self.assertEqual(2, models.File.objects.all().count())
self.assertEqual(1, models.FileContent.objects.all().count())
def test_get_no_files(self):
request = self.client.get('/api/v1/files/')
self.assertEqual(0, len(request.data['results']))
def test_get_files(self):
file = factories.FileFactory()
request = self.client.get('/api/v1/files/')
self.assertEqual(1, len(request.data['results']))
self.assertEqual(file.path, request.data['results'][0]['path'])
def test_delete_file(self):
file = factories.FileFactory()
self.assertEqual(1, models.File.objects.all().count())
request = self.client.delete('/api/v1/files/%s/' % file.id)
self.assertEqual(204, request.status_code)
self.assertEqual(0, models.File.objects.all().count())
def test_create_file(self):
self.assertEqual(0, models.File.objects.count())
request = self.client.post('/api/v1/files/', {
'path': '/tmp/playbook.yml',
'content': '# playbook'
})
self.assertEqual(201, request.status_code)
self.assertEqual(1, models.File.objects.count())
def test_partial_update_file(self):
file = factories.FileFactory()
self.assertNotEqual('/tmp/new_playbook.yml', file.path)
request = self.client.patch('/api/v1/files/%s/' % file.id, {
"path": "/tmp/new_playbook.yml",
})
self.assertEqual(200, request.status_code)
file_updated = models.File.objects.get(id=file.id)
self.assertEqual('/tmp/new_playbook.yml', file_updated.path)
def test_get_file(self):
file = factories.FileFactory()
request = self.client.get('/api/v1/files/%s/' % file.id)
self.assertEqual(file.path, request.data['path'])

View File

@ -1,6 +1,6 @@
from rest_framework.test import APITestCase
from api import serializers
from api import serializers, models
from api.tests import factories
@ -8,16 +8,3 @@ class FileContentTestCase(APITestCase):
def test_file_content_factory(self):
file_content = factories.FileContentFactory(sha1='413a2f16b8689267b7d0c2e10cdd19bf3e54208d')
self.assertEqual(file_content.sha1, '413a2f16b8689267b7d0c2e10cdd19bf3e54208d')
def test_file_content_serializer_compress_contents(self):
serializer = serializers.FileContentSerializer(data={'contents': '# playbook'})
serializer.is_valid()
file_content = serializer.save()
file_content.refresh_from_db()
self.assertEqual(file_content.sha1, '1e58ead094c920fad631d2c22df34dc0314dab0c')
self.assertEqual(file_content.contents, b'x\x9cSV(\xc8I\xacL\xca\xcf\xcf\x06\x00\x11\xbd\x03\xa5')
def test_file_content_serializer_decompress_contents(self):
file_content = factories.FileContentFactory(contents=b'x\x9cSV(\xc8I\xacL\xca\xcf\xcf\x06\x00\x11\xbd\x03\xa5')
serializer = serializers.FileContentSerializer(instance=file_content)
self.assertEqual(serializer.data['contents'], '# playbook')

View File

@ -1,8 +1,6 @@
import datetime
from django.utils import timezone
from rest_framework.test import APITestCase
from rest_framework.test import APIRequestFactory
from rest_framework.request import Request
from api import models, serializers
from api.tests import factories
@ -10,24 +8,20 @@ from api.tests import factories
class PlaybookTestCase(APITestCase):
def test_playbook_factory(self):
playbook = factories.PlaybookFactory(path='/tmp/playbook.yml', ansible_version='2.4.0')
self.assertEqual(playbook.path, '/tmp/playbook.yml')
playbook = factories.PlaybookFactory(ansible_version='2.4.0')
self.assertEqual(playbook.ansible_version, '2.4.0')
def test_playbook_serializer(self):
serializer = serializers.PlaybookSerializer(data={
'path': '/tmp/playbook.yml',
'ansible_version': '2.4.0'
})
serializer.is_valid()
playbook = serializer.save()
playbook.refresh_from_db()
self.assertEqual(playbook.path, '/tmp/playbook.yml')
self.assertEqual(playbook.ansible_version, '2.4.0')
def test_playbook_serializer_compress_parameters(self):
serializer = serializers.PlaybookSerializer(data={
'path': '/tmp/playbook.yml',
'ansible_version': '2.4.0',
'parameters': {'foo': 'bar'}
})
@ -38,9 +32,7 @@ class PlaybookTestCase(APITestCase):
def test_playbook_serializer_decompress_parameters(self):
playbook = factories.PlaybookFactory(parameters=b'x\x9c\xabVJ\xcb\xcfW\xb2RPJJ,R\xaa\x05\x00 \x98\x04T')
serializer = serializers.PlaybookSerializer(instance=playbook, context={
'request': Request(APIRequestFactory().get('/')),
})
serializer = serializers.PlaybookSerializer(instance=playbook)
self.assertEqual(serializer.data['parameters'], {'foo': 'bar'})
def test_get_no_playbooks(self):
@ -51,7 +43,7 @@ class PlaybookTestCase(APITestCase):
playbook = factories.PlaybookFactory()
request = self.client.get('/api/v1/playbooks/')
self.assertEqual(1, len(request.data['results']))
self.assertEqual(playbook.path, request.data['results'][0]['path'])
self.assertEqual(playbook.ansible_version, request.data['results'][0]['ansible_version'])
def test_delete_playbook(self):
playbook = factories.PlaybookFactory()
@ -62,30 +54,25 @@ class PlaybookTestCase(APITestCase):
def test_create_playbook(self):
self.assertEqual(0, models.Playbook.objects.count())
playbook = {
"path": "/tmp/playbook.yml",
request = self.client.post('/api/v1/playbooks/', {
"ansible_version": "2.4.0",
}
request = self.client.post('/api/v1/playbooks/', playbook)
})
self.assertEqual(201, request.status_code)
self.assertEqual(1, models.Playbook.objects.count())
def test_update_playbook(self):
playbook = factories.PlaybookFactory()
self.assertNotEqual('/home/ara/playbook.yml', playbook.path)
new_playbook = {
"path": "/home/ara/playbook.yml",
"ansible_version": "2.4.0",
}
request = self.client.put('/api/v1/playbooks/%s/' % playbook.id, new_playbook)
self.assertNotEqual('2.3.0', playbook.ansible_version)
request = self.client.put('/api/v1/playbooks/%s/' % playbook.id, {
"ansible_version": "2.3.0",
})
self.assertEqual(200, request.status_code)
playbook_updated = models.Playbook.objects.get(id=playbook.id)
self.assertEqual('/home/ara/playbook.yml', playbook_updated.path)
self.assertEqual('2.3.0', playbook_updated.ansible_version)
def test_get_playbook(self):
playbook = factories.PlaybookFactory()
request = self.client.get('/api/v1/playbooks/%s/' % playbook.id)
self.assertEqual(playbook.path, request.data['path'])
self.assertEqual(playbook.ansible_version, request.data['ansible_version'])
def test_get_playbook_duration(self):

View File

@ -0,0 +1,21 @@
import datetime
from django.utils import timezone
from rest_framework.test import APITestCase
from api import models, serializers
from api.tests import factories
class PlaybookFileTestCase(APITestCase):
def test_create_a_file_and_a_playbook_directly(self):
self.assertEqual(0, models.Playbook.objects.all().count())
self.assertEqual(0, models.File.objects.all().count())
self.client.post('/api/v1/playbooks/', {
'ansible_version': '2.4.0',
'files': [{
'path': '/tmp/playbook.yml',
'content': '# playbook'
}],
})
self.assertEqual(1, models.Playbook.objects.all().count())
self.assertEqual(1, models.File.objects.all().count())

View File

@ -46,13 +46,26 @@ playbook = post(
'/api/v1/playbooks/',
{
'started': '2016-05-06T17:20:25.749489-04:00',
'path': '/tmp/playbook.yml',
'ansible_version': '2.3.4',
'completed': False,
'parameters': {'foo': 'bar'}
}
)
playbook_with_files = post(
'/api/v1/playbooks/',
{
'started': '2016-05-06T17:20:25.749489-04:00',
'ansible_version': '2.3.4',
'completed': False,
'parameters': {'foo': 'bar'},
'files': [
{'path': '/tmp/1/playbook.yml', 'content': '# playbook'},
{'path': '/tmp/2/playbook.yml', 'content': '# playbook'}
],
}
)
play = post(
'/api/v1/plays/',
{
@ -61,40 +74,3 @@ play = post(
'playbook': playbook['id']
}
)
playbook_file = post(
'/api/v1/files/',
{
'path': playbook['path'],
# TODO: Fix this somehow
'content': '# playbook',
'playbook': playbook['id'],
'is_playbook': True
}
)
task_file = post(
'/api/v1/files/',
{
'playbook': playbook['id'],
'path': '/tmp/task.yml',
# TODO: Fix this somehow
'content': '# task',
'is_playbook': True
}
)
task = post(
'/api/v1/tasks/',
{
'playbook': playbook['id'],
'play': play['id'],
'file': task_file['id'],
'name': 'Task name',
'action': 'action',
'lineno': 1,
'tags': ['one', 'two'],
'handler': False,
'started': '2016-05-06T17:20:25.749489-04:00'
}
)