diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py index 31f6fa8..e6eb9df 100644 --- a/api/migrations/0001_initial.py +++ b/api/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.3 on 2018-03-20 13:55 +# Generated by Django 2.0.3 on 2018-03-20 18:21 from django.db import migrations, models import django.db.models.deletion @@ -81,6 +81,7 @@ class Migration(migrations.Migration): ('ansible_version', models.CharField(max_length=255)), ('completed', models.BooleanField(default=False)), ('parameters', models.BinaryField(max_length=4294967295)), + ('file', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='playbooks', to='api.File')), ('files', models.ManyToManyField(to='api.File')), ], options={ @@ -112,6 +113,7 @@ class Migration(migrations.Migration): ('ended', models.DateTimeField(blank=True, null=True)), ('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)), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='api.Host')), ], options={ 'db_table': 'results', @@ -130,39 +132,29 @@ class Migration(migrations.Migration): ('lineno', models.IntegerField()), ('tags', models.BinaryField(max_length=4294967295)), ('handler', models.BooleanField()), + ('file', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 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')), - ('results', models.ManyToManyField(to='api.Result')), ], options={ 'db_table': 'tasks', }, ), migrations.AddField( - model_name='playbook', - name='results', - field=models.ManyToManyField(to='api.Result'), + model_name='result', + name='task', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='api.Task'), ), 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', field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='hosts', to='api.Play'), ), - migrations.AddField( - model_name='host', - name='results', - field=models.ManyToManyField(to='api.Result'), - ), migrations.AddField( model_name='file', name='content', diff --git a/api/models.py b/api/models.py index 51dc595..7e2e778 100644 --- a/api/models.py +++ b/api/models.py @@ -77,42 +77,6 @@ class File(Base): return '' % (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 '' % (self.id, self.status) - - class Playbook(Duration): """ An entry in the 'playbooks' table represents a single execution of the @@ -126,8 +90,8 @@ class Playbook(Duration): ansible_version = models.CharField(max_length=255) completed = models.BooleanField(default=False) parameters = models.BinaryField(max_length=(2 ** 32) - 1) + file = models.ForeignKey(File, on_delete=models.CASCADE, related_name='playbooks') files = models.ManyToManyField(File) - results = models.ManyToManyField(Result) def __str__(self): return '' % self.id @@ -163,7 +127,6 @@ class Play(Duration): 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 '' % (self.id, self.name) @@ -182,7 +145,7 @@ class Task(Duration): handler = models.BooleanField() play = models.ForeignKey(Play, on_delete=models.CASCADE, related_name='tasks') - results = models.ManyToManyField(Result) + file = models.ForeignKey(File, on_delete=models.CASCADE, related_name='tasks') files = models.ManyToManyField(File) def __str__(self): @@ -208,7 +171,45 @@ class Host(Base): 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 '' % (self.id, self.name) + + +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) + # todo use a single Content table + content = models.BinaryField(max_length=(2 ** 32) - 1) + host = models.ForeignKey(Host, on_delete=models.CASCADE, related_name='results') + task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name='results') + + def __str__(self): + return '' % (self.id, self.status) diff --git a/api/serializers.py b/api/serializers.py index 3e7a9a4..ad29d67 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -116,24 +116,19 @@ class PlaybookSerializer(DurationSerializer): model = models.Playbook fields = '__all__' - parameters = CompressedObjectField( - default=zlib.compress(json.dumps({}).encode('utf8')), - help_text='A JSON dictionary containing Ansible command parameters' - ) + parameters = CompressedObjectField(default=zlib.compress(json.dumps({}).encode('utf8'))) + file = FileSerializer() files = FileSerializer(many=True, default=[]) - results = ResultSerializer(read_only=True, many=True) def create(self, validated_data): + file_dict = validated_data.pop('file') + validated_data['file'] = models.File.objects.create(**file_dict) 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): class Meta: diff --git a/api/tests/factories.py b/api/tests/factories.py index 90ea8eb..dad10b3 100644 --- a/api/tests/factories.py +++ b/api/tests/factories.py @@ -3,15 +3,6 @@ import factory from api import models -class PlaybookFactory(factory.DjangoModelFactory): - class Meta: - model = models.Playbook - - 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 @@ -27,3 +18,13 @@ class FileFactory(factory.DjangoModelFactory): path = '/tmp/playbook.yml' content = factory.SubFactory(FileContentFactory) + + +class PlaybookFactory(factory.DjangoModelFactory): + class Meta: + model = models.Playbook + + ansible_version = '2.4.0' + completed = True + parameters = b'x\x9c\xabVJ\xcb\xcfW\xb2RPJJ,R\xaa\x05\x00 \x98\x04T' + file = factory.SubFactory(FileFactory) diff --git a/api/tests/tests_file.py b/api/tests/tests_file.py index d2daeec..c5a6c73 100644 --- a/api/tests/tests_file.py +++ b/api/tests/tests_file.py @@ -43,6 +43,15 @@ class FileTestCase(APITestCase): self.assertEqual(2, models.File.objects.all().count()) self.assertEqual(1, models.FileContent.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_get_no_files(self): request = self.client.get('/api/v1/files/') self.assertEqual(0, len(request.data['results'])) @@ -53,21 +62,21 @@ class FileTestCase(APITestCase): self.assertEqual(1, len(request.data['results'])) self.assertEqual(file.path, request.data['results'][0]['path']) - def test_delete_file(self): + def test_get_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()) + request = self.client.get('/api/v1/files/%s/' % file.id) + self.assertEqual(file.path, request.data['path']) - def test_create_file(self): - self.assertEqual(0, models.File.objects.count()) - request = self.client.post('/api/v1/files/', { - 'path': '/tmp/playbook.yml', + def test_update_file(self): + file = factories.FileFactory() + self.assertNotEqual('/tmp/new_playbook.yml', file.path) + request = self.client.put('/api/v1/files/%s/' % file.id, { + "path": "/tmp/new_playbook.yml", 'content': '# playbook' }) - self.assertEqual(201, request.status_code) - self.assertEqual(1, models.File.objects.count()) + 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_partial_update_file(self): file = factories.FileFactory() @@ -79,7 +88,9 @@ class FileTestCase(APITestCase): file_updated = models.File.objects.get(id=file.id) self.assertEqual('/tmp/new_playbook.yml', file_updated.path) - def test_get_file(self): + def test_delete_file(self): file = factories.FileFactory() - request = self.client.get('/api/v1/files/%s/' % file.id) - self.assertEqual(file.path, request.data['path']) + 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()) diff --git a/api/tests/tests_playbook.py b/api/tests/tests_playbook.py index 4196e76..135f86b 100644 --- a/api/tests/tests_playbook.py +++ b/api/tests/tests_playbook.py @@ -13,7 +13,11 @@ class PlaybookTestCase(APITestCase): def test_playbook_serializer(self): serializer = serializers.PlaybookSerializer(data={ - 'ansible_version': '2.4.0' + 'ansible_version': '2.4.0', + 'file': { + 'path': '/tmp/playbook.yml', + 'content': '# playbook' + } }) serializer.is_valid() playbook = serializer.save() @@ -23,6 +27,10 @@ class PlaybookTestCase(APITestCase): def test_playbook_serializer_compress_parameters(self): serializer = serializers.PlaybookSerializer(data={ 'ansible_version': '2.4.0', + 'file': { + 'path': '/tmp/playbook.yml', + 'content': '# playbook' + }, 'parameters': {'foo': 'bar'} }) serializer.is_valid() @@ -56,14 +64,18 @@ class PlaybookTestCase(APITestCase): self.assertEqual(0, models.Playbook.objects.count()) request = self.client.post('/api/v1/playbooks/', { "ansible_version": "2.4.0", + 'file': { + 'path': '/tmp/playbook.yml', + 'content': '# playbook' + } }) self.assertEqual(201, request.status_code) self.assertEqual(1, models.Playbook.objects.count()) - def test_update_playbook(self): + def test_partial_update_playbook(self): playbook = factories.PlaybookFactory() self.assertNotEqual('2.3.0', playbook.ansible_version) - request = self.client.put('/api/v1/playbooks/%s/' % playbook.id, { + request = self.client.patch('/api/v1/playbooks/%s/' % playbook.id, { "ansible_version": "2.3.0", }) self.assertEqual(200, request.status_code) diff --git a/api/tests/tests_playbook_file.py b/api/tests/tests_playbook_file.py index 441c88c..20433e6 100644 --- a/api/tests/tests_playbook_file.py +++ b/api/tests/tests_playbook_file.py @@ -12,10 +12,37 @@ class PlaybookFileTestCase(APITestCase): self.assertEqual(0, models.File.objects.all().count()) self.client.post('/api/v1/playbooks/', { 'ansible_version': '2.4.0', - 'files': [{ + 'file': { 'path': '/tmp/playbook.yml', 'content': '# playbook' + }, + 'files': [{ + 'path': '/tmp/host', + 'content': '# host' }], }) self.assertEqual(1, models.Playbook.objects.all().count()) + self.assertEqual(2, models.File.objects.all().count()) + + def test_create_file_to_a_playbook(self): + playbook = factories.PlaybookFactory() + self.assertEqual(0, models.File.objects.all().count()) + self.client.post('/api/v1/playbooks/%s/files' % playbook.id, { + 'path': '/tmp/playbook.yml', + 'content': '# playbook' + }) self.assertEqual(1, models.File.objects.all().count()) + self.assertEqual(1, models.FileContent.objects.all().count()) + + def test_create_2_files_with_same_content(self): + playbook = factories.PlaybookFactory() + self.client.post('/api/v1/playbooks/%s/files' % playbook.id, { + 'path': '/tmp/1/playbook.yml', + 'content': '# playbook' + }) + self.client.post('/api/v1/playbooks/%s/files' % playbook.id, { + 'path': '/tmp/2/playbook.yml', + 'content': '# playbook' + }) + self.assertEqual(2, models.File.objects.all().count()) + self.assertEqual(1, models.FileContent.objects.all().count())