diff --git a/ara/api/migrations/0002_add_reports.py b/ara/api/migrations/0002_add_reports.py new file mode 100644 index 0000000..d420c24 --- /dev/null +++ b/ara/api/migrations/0002_add_reports.py @@ -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'), + ), + ] diff --git a/ara/api/models.py b/ara/api/models.py index 4568868..eb1c769 100644 --- a/ara/api/models.py +++ b/ara/api/models.py @@ -77,6 +77,28 @@ class File(Base): return '' % (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 '' % (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 '' % self.id diff --git a/ara/api/serializers.py b/ara/api/serializers.py index 469a560..ac7710a 100644 --- a/ara/api/serializers.py +++ b/ara/api/serializers.py @@ -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 diff --git a/ara/api/tests/factories.py b/ara/api/tests/factories.py index 9adbd7d..62532e6 100644 --- a/ara/api/tests/factories.py +++ b/ara/api/tests/factories.py @@ -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 diff --git a/ara/api/tests/tests_playbook.py b/ara/api/tests/tests_playbook.py index 67f1851..0d6f2d4 100644 --- a/ara/api/tests/tests_playbook.py +++ b/ara/api/tests/tests_playbook.py @@ -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 diff --git a/ara/api/tests/tests_report.py b/ara/api/tests/tests_report.py new file mode 100644 index 0000000..aac9894 --- /dev/null +++ b/ara/api/tests/tests_report.py @@ -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()) diff --git a/ara/api/urls.py b/ara/api/urls.py index 52948d8..0de4d2a 100644 --- a/ara/api/urls.py +++ b/ara/api/urls.py @@ -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[0-9]+)/$', views.ReportDetail.as_view(), name='report-detail'), url(r'^playbooks/$', views.PlaybookList.as_view(), name='playbook-list'), url(r'^playbooks/(?P[0-9]+)/$', views.PlaybookDetail.as_view(), name='playbook-detail'), url(r'^playbooks/(?P[0-9]+)/files/$', views.PlaybookFilesDetail.as_view(), name='playbook-file-detail'), diff --git a/ara/api/views.py b/ara/api/views.py index 474c6ba..a2544c0 100644 --- a/ara/api/views.py +++ b/ara/api/views.py @@ -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