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:
parent
69cc8dcac1
commit
17f6c9eca5
|
@ -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')},
|
||||
),
|
||||
]
|
||||
|
|
241
api/models.py
241
api/models.py
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'])
|
|
@ -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')
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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())
|
|
@ -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'
|
||||
}
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue