Add the concept of reports to the API

A report is a generic container meant to group or correlate different
playbooks.
It could contain a single playbook run or a group of playbook runs.
It can also be used to represent phases or dynamic tagging of playbook
runs.
For example, you could have a report named "failures" and make it so
failed playbooks are added to this report, for example.

The main purpose of this is to make reports generic and dynamic from
an API standpoint so we can build on top of this later.

Change-Id: I398f0337987abe31fa1e886f66ec9c3e644a32d6
This commit is contained in:
David Moreau Simard 2018-06-20 23:40:00 -04:00
parent dd4da299f8
commit 50f322932c
No known key found for this signature in database
GPG Key ID: 33A07694CBB71ECC
8 changed files with 175 additions and 0 deletions

View File

@ -0,0 +1,31 @@
# Generated by Django 2.0.6 on 2018-06-21 03:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Report',
fields=[
('id', models.BigAutoField(editable=False, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=255)),
('description', models.BinaryField(max_length=4294967295)),
],
options={
'db_table': 'reports',
},
),
migrations.AddField(
model_name='playbook',
name='reports',
field=models.ManyToManyField(to='api.Report'),
),
]

View File

@ -77,6 +77,28 @@ class File(Base):
return '<File %s:%s>' % (self.id, self.path)
class Report(Base):
"""
A report is a generic container meant to group or correlate different
playbooks. It could be a single playbook run. It could be a "group" of
playbooks. It could be nested and have several groups of playbooks, etc.
It could represent phases or dynamic logical grouping and tagging of
playbook runs.
You could have a report named "failures" and make it so failed playbooks
are added to this report, for example.
The main purpose of this is to make the reports customizable by the user.
"""
class Meta:
db_table = 'reports'
name = models.CharField(max_length=255)
description = models.BinaryField(max_length=(2 ** 32) - 1)
def __str__(self):
return '<Report %s: %s>' % (self.id, self.name)
class Playbook(Duration):
"""
An entry in the 'playbooks' table represents a single execution of the
@ -92,6 +114,7 @@ class Playbook(Duration):
parameters = models.BinaryField(max_length=(2 ** 32) - 1)
file = models.ForeignKey(File, on_delete=models.CASCADE, related_name='playbooks')
files = models.ManyToManyField(File)
reports = models.ManyToManyField(Report)
def __str__(self):
return '<Playbook %s>' % self.id

View File

@ -121,6 +121,17 @@ class ResultSerializer(serializers.ModelSerializer):
content = CompressedObjectField(default=zlib.compress(json.dumps({}).encode('utf8')))
class ReportSerializer(serializers.ModelSerializer):
class Meta:
model = models.Report
fields = '__all__'
description = CompressedTextField(
default=zlib.compress(json.dumps("").encode('utf8')),
help_text='A textual description of the report'
)
class PlaybookSerializer(DurationSerializer):
class Meta:
model = models.Playbook
@ -129,14 +140,24 @@ class PlaybookSerializer(DurationSerializer):
parameters = CompressedObjectField(default=zlib.compress(json.dumps({}).encode('utf8')))
file = FileSerializer()
files = FileSerializer(many=True, default=[])
reports = ReportSerializer(many=True, default=[])
def create(self, validated_data):
# Create the file for the playbook
file_dict = validated_data.pop('file')
validated_data['file'] = models.File.objects.create(**file_dict)
# Create the playbook without the file and report references for now
files = validated_data.pop('files')
reports = validated_data.pop('reports')
playbook = models.Playbook.objects.create(**validated_data)
# Add the files and the reports in
for file in files:
playbook.files.add(models.File.objects.create(**file))
for report in reports:
playbook.reports.add(models.Report.objects.create(**report))
return playbook

View File

@ -20,6 +20,14 @@ class FileFactory(factory.DjangoModelFactory):
content = factory.SubFactory(FileContentFactory)
class ReportFactory(factory.DjangoModelFactory):
class Meta:
model = models.Report
name = 'test report'
description = b'x\x9cKI-N.\xca,(\xc9\xcc\xcf\x03\x00\x1b\x87\x04\xa5' # 'description'
class PlaybookFactory(factory.DjangoModelFactory):
class Meta:
model = models.Playbook

View File

@ -98,3 +98,5 @@ class PlaybookTestCase(APITestCase):
playbook = factories.PlaybookFactory(started=started, ended=ended)
request = self.client.get('/api/v1/playbooks/%s/' % playbook.id)
self.assertEqual(request.data['duration'], datetime.timedelta(0, 3600))
# TODO: Add tests for incrementally updating files

View File

@ -0,0 +1,77 @@
from rest_framework.test import APITestCase
from ara.api import models, serializers
from ara.api.tests import factories
class ReportTestCase(APITestCase):
def test_report_factory(self):
report = factories.ReportFactory(name='factory')
self.assertEqual(report.name, 'factory')
def test_report_serializer(self):
serializer = serializers.ReportSerializer(data={
'name': 'serializer',
})
serializer.is_valid()
report = serializer.save()
report.refresh_from_db()
self.assertEqual(report.name, 'serializer')
def test_report_serializer_compress_description(self):
serializer = serializers.ReportSerializer(data={
'name': 'compress',
'description': 'description'
})
serializer.is_valid()
report = serializer.save()
report.refresh_from_db()
self.assertEqual(report.description, b'x\x9cKI-N.\xca,(\xc9\xcc\xcf\x03\x00\x1b\x87\x04\xa5') # 'description'
def test_report_serializer_decompress_parameters(self):
report = factories.ReportFactory(
description=b'x\x9cKI-N.\xca,(\xc9\xcc\xcf\x03\x00\x1b\x87\x04\xa5' # 'description'
)
serializer = serializers.ReportSerializer(instance=report)
self.assertEqual(serializer.data['description'], 'description')
def test_create_report(self):
self.assertEqual(0, models.Report.objects.count())
request = self.client.post('/api/v1/reports/', {
'name': 'compress',
'description': 'description'
})
self.assertEqual(201, request.status_code)
self.assertEqual(1, models.Report.objects.count())
def test_get_no_reports(self):
request = self.client.get('/api/v1/reports/')
self.assertEqual(0, len(request.data['results']))
def test_get_reports(self):
report = factories.ReportFactory()
request = self.client.get('/api/v1/reports/')
self.assertEqual(1, len(request.data['results']))
self.assertEqual(report.name, request.data['results'][0]['name'])
def test_get_report(self):
report = factories.ReportFactory()
request = self.client.get('/api/v1/reports/%s/' % report.id)
self.assertEqual(report.name, request.data['name'])
def test_partial_update_report(self):
report = factories.ReportFactory()
self.assertNotEqual('updated', report.name)
request = self.client.patch('/api/v1/reports/%s/' % report.id, {
'name': 'updated'
})
self.assertEqual(200, request.status_code)
report_updated = models.Report.objects.get(id=report.id)
self.assertEqual('updated', report_updated.name)
def test_delete_report(self):
report = factories.ReportFactory()
self.assertEqual(1, models.Report.objects.all().count())
request = self.client.delete('/api/v1/reports/%s/' % report.id)
self.assertEqual(204, request.status_code)
self.assertEqual(0, models.Report.objects.all().count())

View File

@ -21,6 +21,8 @@ from ara.api import views
urlpatterns = [
url(r'^$', views.api_root),
url(r'^reports/$', views.ReportList.as_view(), name='report-list'),
url(r'^reports/(?P<pk>[0-9]+)/$', views.ReportDetail.as_view(), name='report-detail'),
url(r'^playbooks/$', views.PlaybookList.as_view(), name='playbook-list'),
url(r'^playbooks/(?P<pk>[0-9]+)/$', views.PlaybookDetail.as_view(), name='playbook-detail'),
url(r'^playbooks/(?P<pk>[0-9]+)/files/$', views.PlaybookFilesDetail.as_view(), name='playbook-file-detail'),

View File

@ -26,6 +26,7 @@ from rest_framework import generics, status
@api_view(['GET'])
def api_root(request, format=None):
return Response({
'reports': reverse('report-list', request=request, format=format),
'playbooks': reverse('playbook-list', request=request, format=format),
'plays': reverse('play-list', request=request, format=format),
'tasks': reverse('task-list', request=request, format=format),
@ -35,6 +36,16 @@ def api_root(request, format=None):
})
class ReportList(generics.ListCreateAPIView):
queryset = models.Report.objects.all()
serializer_class = serializers.ReportSerializer
class ReportDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = models.Report.objects.all()
serializer_class = serializers.ReportSerializer
class PlaybookList(generics.ListCreateAPIView):
queryset = models.Playbook.objects.all()
serializer_class = serializers.PlaybookSerializer