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:
parent
dd4da299f8
commit
50f322932c
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
|
@ -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'),
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue