From e6a39c96631a1d9a643430c313b6c60af0715416 Mon Sep 17 00:00:00 2001 From: David Moreau Simard Date: Thu, 8 Mar 2018 18:22:22 -0500 Subject: [PATCH] Update model and serializers to 1.0 schema The proof of concept/prototype was built using a database schema that was closer to what the stable release of ARA used instead of the one that had been worked on already for 1.0. This updates the models and API serializers to reflect that and be more accurate. Change-Id: I963823e84e2d2ef9f6262cd04de8fd31ffc7d5b4 --- api/migrations/0001_initial.py | 130 ++++++++++++------- api/models.py | 229 +++++++++++++++++++++++++++------ api/serializers.py | 197 +++++++++++++++++++++++++++- api/urls.py | 24 ++-- api/views.py | 5 + ara/urls.py | 2 +- 6 files changed, 485 insertions(+), 102 deletions(-) diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py index f43c596..0ffba09 100644 --- a/api/migrations/0001_initial.py +++ b/api/migrations/0001_initial.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.8 on 2018-02-27 12:25 -from __future__ import unicode_literals +# Generated by Django 2.0.3 on 2018-03-10 16:49 from django.db import migrations, models import django.db.models.deletion -import uuid +import django.utils.timezone class Migration(migrations.Migration): @@ -18,22 +16,37 @@ class Migration(migrations.Migration): migrations.CreateModel( name='File', fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('content', models.FileField(upload_to='files/%Y/%m/%d/')), + ('id', models.BigAutoField(editable=False, primary_key=True, serialize=False)), + ('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={ - 'abstract': False, + 'db_table': 'files', + }, + ), + migrations.CreateModel( + name='FileContent', + fields=[ + ('id', models.BigAutoField(editable=False, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('sha1', models.CharField(max_length=40, unique=True)), + ('contents', models.BinaryField(max_length=4294967295)), + ], + options={ + 'db_table': 'file_contents', }, ), migrations.CreateModel( name='Host', fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('start', models.DateTimeField(auto_now_add=True, verbose_name='started')), - ('ended', models.DateTimeField(auto_now=True, verbose_name='ended')), + ('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)), - ('facts', models.TextField(blank=True, null=True)), + ('facts', models.BinaryField(max_length=4294967295)), ('changed', models.IntegerField(default=0)), ('failed', models.IntegerField(default=0)), ('ok', models.IntegerField(default=0)), @@ -41,100 +54,127 @@ class Migration(migrations.Migration): ('unreachable', models.IntegerField(default=0)), ], options={ - 'abstract': False, + 'db_table': 'hosts', }, ), migrations.CreateModel( name='Play', fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('start', models.DateTimeField(auto_now_add=True, verbose_name='started')), - ('ended', models.DateTimeField(auto_now=True, verbose_name='ended')), - ('name', models.TextField(blank=True, null=True)), + ('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(default=django.utils.timezone.now)), + ('ended', models.DateTimeField(blank=True, null=True)), + ('name', models.TextField()), ], options={ - 'abstract': False, + 'db_table': 'plays', }, ), migrations.CreateModel( name='Playbook', fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('start', models.DateTimeField(auto_now_add=True, verbose_name='started')), - ('ended', models.DateTimeField(auto_now=True, verbose_name='ended')), + ('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(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.TextField(blank=True, null=True)), + ('parameters', models.BinaryField(max_length=4294967295)), ('completed', models.BooleanField(default=False)), ], options={ - 'abstract': False, + 'db_table': 'playbooks', }, ), migrations.CreateModel( name='Record', fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('start', models.DateTimeField(auto_now_add=True, verbose_name='started')), - ('ended', models.DateTimeField(auto_now=True, verbose_name='ended')), + ('id', models.BigAutoField(editable=False, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), ('key', models.CharField(max_length=255)), ('value', models.TextField(blank=True, null=True)), ('type', models.CharField(max_length=255)), - ('playbook_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records', to='api.Playbook')), + ('playbook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records', to='api.Playbook')), ], options={ - 'abstract': False, + 'db_table': 'records', }, ), migrations.CreateModel( name='Result', fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('start', models.DateTimeField(auto_now_add=True, verbose_name='started')), - ('ended', models.DateTimeField(auto_now=True, verbose_name='ended')), - ('status', models.CharField(blank=True, choices=[('ok', 'ok'), ('failed', 'failed'), ('skipped', 'skipped'), ('unreachable', 'unreachable')], max_length=11, null=True)), + ('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(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.TextField(blank=True, null=True)), - ('playbook_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='api.Playbook')), + ('result', models.BinaryField(max_length=4294967295)), + ('playbook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='api.Playbook')), ], options={ - 'abstract': False, + 'db_table': 'results', }, ), migrations.CreateModel( name='Task', fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('start', models.DateTimeField(auto_now_add=True, verbose_name='started')), - ('ended', models.DateTimeField(auto_now=True, verbose_name='ended')), - ('name', models.TextField(blank=True, null=True)), - ('action', models.TextField(blank=True, null=True)), + ('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(default=django.utils.timezone.now)), + ('ended', models.DateTimeField(blank=True, null=True)), + ('name', models.TextField()), + ('action', models.TextField()), ('lineno', models.IntegerField()), - ('tags', models.TextField(blank=True, null=True)), + ('tags', models.BinaryField(max_length=4294967295)), ('handler', models.BooleanField()), - ('playbook_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='api.Playbook')), + ('file', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks', to='api.File')), + ('play', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks', to='api.Play')), + ('playbook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='api.Playbook')), ], options={ - 'abstract': False, + 'db_table': 'tasks', }, ), migrations.AddField( model_name='play', - name='playbook_id', + name='playbook', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plays', to='api.Playbook'), ), migrations.AddField( model_name='host', - name='playbook_id', + name='playbook', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hosts', to='api.Playbook'), ), migrations.AddField( model_name='file', - name='playbook_id', + 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')}, + ), ] diff --git a/api/models.py b/api/models.py index afedf89..5bbd575 100644 --- a/api/models.py +++ b/api/models.py @@ -1,104 +1,251 @@ -import uuid +# Copyright (c) 2018 Red Hat, Inc. +# +# This file is part of ARA Records Ansible. +# +# ARA is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ARA is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with ARA. If not, see . + +import logging from django.db import models +from django.utils import timezone + +logger = logging.getLogger('ara_backend.models') class Base(models.Model): - 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, editable=False) + updated = models.DateTimeField(auto_now=True, editable=False) + + @property + def age(self): + """ + Calculates duration between created and updated. + """ + return self.updated - self.created class Meta: abstract = True -class DateMixin(Base): - start = models.DateTimeField(auto_now_add=True, verbose_name='started') - ended = models.DateTimeField(auto_now=True, verbose_name='ended') +class DurationMixin(models.Model): + started = models.DateTimeField(default=timezone.now) + ended = models.DateTimeField(blank=True, null=True) + + @property + def duration(self): + """ + Calculates duration between started and ended or between started and + updated if we do not yet have an end. + """ + if self.ended is None: + if self.started is None: + return timezone.timedelta(seconds=0) + else: + return self.updated - self.started + return self.ended - self.started class Meta: abstract = True -class Playbook(DateMixin): +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.TextField(null=True, blank=True) + parameters = models.BinaryField(max_length=(2 ** 32) - 1) completed = models.BooleanField(default=False) def __str__(self): - return '' % self.id + return '' % (self.id, self.path) + __repr__ = __str__ + + +class FileContent(Base): + class Meta: + db_table = 'file_contents' + + sha1 = models.CharField(max_length=40, unique=True) + contents = models.BinaryField(max_length=(2 ** 32) - 1) + + def __str__(self): + return '' % (self.id, self.sha1) + __repr__ = __str__ class File(Base): - content = models.FileField(upload_to='files/%Y/%m/%d/') + class Meta: + db_table = 'files' + unique_together = ('path', 'playbook',) + + path = models.CharField(max_length=255) is_playbook = models.BooleanField(default=False) - playbook_id = models.ForeignKey(Playbook, on_delete=models.CASCADE, related_name='files') + content = models.ForeignKey(FileContent, + on_delete=models.CASCADE, + related_name='files') + playbook = models.ForeignKey(Playbook, + on_delete=models.CASCADE, + related_name='files') + + def __str__(self): + return '' % (self.id, self.path) + __repr__ = __str__ -class Record(DateMixin): +class Record(Base): + class Meta: + db_table = 'records' + unique_together = ('key', 'playbook',) + key = models.CharField(max_length=255) value = models.TextField(null=True, blank=True) type = models.CharField(max_length=255) - playbook_id = 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 '' % self.id + return '' % (self.id, self.key) + __repr__ = __str__ -class Host(DateMixin): +class Host(Base): + class Meta: + db_table = 'hosts' + unique_together = ('name', 'playbook',) + name = models.CharField(max_length=255) - facts = models.TextField(null=True, blank=True) + 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_id = models.ForeignKey(Playbook, on_delete=models.CASCADE, related_name='hosts') + playbook = models.ForeignKey(Playbook, + on_delete=models.CASCADE, + related_name='hosts') def __str__(self): - return '' % (self.name, self.id) + return '' % (self.id, self.name) + __repr__ = __str__ -OK = "ok" -FAILED = "failed" -SKIPPED = "skipped" -UNREACHABLE = "unreachable" +class Play(Base, DurationMixin): + class Meta: + db_table = 'plays' -RESULT_STATUS = ( - (OK, "ok"), - (FAILED, "failed"), - (SKIPPED, "skipped"), - (UNREACHABLE, "unreachable") -) + name = models.TextField() + playbook = models.ForeignKey(Playbook, + on_delete=models.CASCADE, + related_name='plays') - -class Play(DateMixin): - name = models.TextField(null=True, blank=True) - playbook_id = models.ForeignKey(Playbook, on_delete=models.CASCADE, related_name='plays') + @property + def offset_from_playbook(self): + return self.started - self.playbook.started def __str__(self): return '' % (self.name, self.id) + __repr__ = __str__ -class Task(DateMixin): - name = models.TextField(null=True, blank=True) - action = models.TextField(null=True, blank=True) +class Task(Base, DurationMixin): + class Meta: + db_table = 'tasks' + + name = models.TextField() + action = models.TextField() lineno = models.IntegerField() - tags = models.TextField(null=True, blank=True) + tags = models.BinaryField(max_length=(2 ** 32) - 1) handler = models.BooleanField() - playbook_id = models.ForeignKey(Playbook, on_delete=models.CASCADE, related_name='tasks') + + playbook = models.ForeignKey(Playbook, + 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.DO_NOTHING, + related_name='tasks') + + @property + def offset_from_playbook(self): + return self.started - self.playbook.started + + @property + def offset_from_play(self): + return self.started - self.play.started def __str__(self): return '' % (self.name, self.id) + __repr__ = __str__ -class Result(DateMixin): - status = models.CharField(max_length=11, choices=RESULT_STATUS, null=True, blank=True) +class Result(Base, DurationMixin): + 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' + + RESULT_STATUS = ( + (OK, 'ok'), + (FAILED, 'failed'), + (SKIPPED, 'skipped'), + (UNREACHABLE, 'unreachable'), + (UNKNOWN, 'unknown') + ) + + 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.TextField(null=True, blank=True) - playbook_id = models.ForeignKey(Playbook, on_delete=models.CASCADE, related_name='results') + result = models.BinaryField(max_length=(2 ** 32) - 1) + playbook = models.ForeignKey(Playbook, + on_delete=models.CASCADE, + related_name='results') + + @property + def derived_status(self): + if self.status == self.OK and self.changed: + return self.CHANGED + elif self.status == self.FAILED and self.ignore_errors: + return self.IGNORED + elif self.status not in [ + self.OK, self.FAILED, self.SKIPPED, self.UNREACHABLE + ]: + return self.UNKNOWN + else: + return self.status def __str__(self): - return '' % self.id + return '' % (self.id, self.derived_status) + __repr__ = __str__ diff --git a/api/serializers.py b/api/serializers.py index 1f5c074..bddf5c9 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,24 +1,207 @@ +# -*- coding: utf-8 -*- +import hashlib +import json +import logging +import zlib from api import models +from django.utils import timezone from rest_framework import serializers +from rest_framework.exceptions import ValidationError + +DATE_FORMAT = "(iso-8601: 2016-05-06T17:20:25.749489-04:00)" +DURATION_FORMAT = "([DD] [HH:[MM:]]ss[.uuuuuu])" +logger = logging.getLogger('api.serializers') -class PlaybookSerializer(serializers.ModelSerializer): +class CompressedTextField(serializers.CharField): + def to_representation(self, obj): + return zlib.decompress(obj).decode('utf8') + + def to_internal_value(self, data): + return zlib.compress(data.encode('utf8')) + + +class CompressedObjectField(serializers.JSONField): + def to_representation(self, obj): + return json.loads(zlib.decompress(obj).decode('utf8')) + + def to_internal_value(self, data): + return zlib.compress(data.encode('utf8')) + + +class SHA1Field(serializers.CharField): + def to_representation(self, obj): + return json.loads(lzma.decompress(obj).decode('utf8')) + + def to_internal_value(self, data): + return lzma.compress(data.encode('utf8')) + + +class BaseSerializer(serializers.ModelSerializer): + """ + Serializer for the data in the model base + """ + 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 + ) + age = serializers.DurationField( + read_only=True, + help_text='Duration since the creation %s' % DURATION_FORMAT + ) + + class Meta: + abstract = True + + +class DurationSerializer(serializers.ModelSerializer): + """ + Serializer for duration-based fields + """ + started = serializers.DateTimeField( + initial=timezone.now().isoformat(), + help_text='Date this item started %s' % DATE_FORMAT + ) + ended = serializers.DateTimeField( + required=False, + help_text='Date this item ended %s' % DATE_FORMAT + ) + duration = serializers.DurationField( + read_only=True, + help_text="Duration between 'started' and 'ended' %s" % DURATION_FORMAT + ) + + def validate(self, data): + """ + Check that the start is before the end. + """ + if 'ended' in data and (data['started'] > data['ended']): + raise serializers.ValidationError( + "'Ended' must be before 'started'" + ) + return data + + class Meta: + abstract = True + + +class PlaybookSerializer(BaseSerializer, DurationSerializer): class Meta: model = models.Playbook fields = '__all__' + plays = serializers.HyperlinkedRelatedField( + many=True, + read_only=True, + view_name='plays', + help_text='Plays associated to this playbook' + ) +# tasks = serializers.HyperlinkedRelatedField( +# many=True, +# read_only=True, +# view_name='tasks', +# help_text='Tasks associated to this playbook' +# ) +# hosts = serializers.HyperlinkedRelatedField( +# many=True, +# read_only=True, +# view_name='hosts', +# help_text='Hosts associated to this playbook' +# ) +# results = serializers.HyperlinkedRelatedField( +# many=True, +# read_only=True, +# view_name='results', +# help_text='Results associated to this playbook' +# ) +# records = serializers.HyperlinkedRelatedField( +# many=True, +# read_only=True, +# view_name='records', +# help_text='Records associated to this playbook' +# ) +# files = serializers.HyperlinkedRelatedField( +# many=True, +# read_only=True, +# view_name='files', +# help_text='Records associated to this playbook' +# ) -class FileSerializer(serializers.ModelSerializer): - playbook = PlaybookSerializer(source='playbook_id', read_only=True) - + parameters = CompressedObjectField( + initial={}, + help_text='A JSON dictionary containing Ansible command parameters' + ) + path = serializers.CharField(help_text='Path to the playbook file') + ansible_version = serializers.CharField( + help_text='Version of Ansible used to run this playbook' + ) + completed = serializers.BooleanField( + help_text='If the completion of the execution has been acknowledged' + ) + + +class PlaySerializer(BaseSerializer, DurationSerializer): class Meta: - model = models.File + model = models.Play fields = '__all__' -class RecordSerializer(serializers.ModelSerializer): - playbook = PlaybookSerializer(source='playbook_id', read_only=True) +class TaskSerializer(BaseSerializer, DurationSerializer): + class Meta: + model = models.Task + fields = '__all__' + +class HostSerializer(BaseSerializer): + class Meta: + model = models.Host + fields = '__all__' + + +class ResultSerializer(BaseSerializer, DurationSerializer): + class Meta: + model = models.Result + fields = '__all__' + + +class RecordSerializer(BaseSerializer): class Meta: model = models.Record fields = '__all__' + + +class FileContentSerializer(BaseSerializer): + class Meta: + model = models.FileContent + fields = ('contents', 'sha1') + + contents = CompressedTextField(help_text='Contents of the file') + sha1 = serializers.CharField(read_only=True, help_text='sha1 of the file') + + def create(self, validated_data): + sha1 = hashlib.sha1(validated_data['contents']).hexdigest() + validated_data['sha1'] = sha1 + obj, created = models.FileContent.objects.get_or_create( + **validated_data + ) + return obj + + +class FileSerializer(BaseSerializer): + path = serializers.CharField(help_text='Path to the file') + content = FileContentSerializer() + + def create(self, validated_data): + contents = validated_data.pop('content')['contents'] + obj, created = models.FileContent.objects.get_or_create( + contents=contents, + sha1=hashlib.sha1(contents).hexdigest() + ) + validated_data['content'] = obj + return models.File.objects.create(**validated_data) + + class Meta: + model = models.File + fields = ('id', 'path', 'content', 'playbook') diff --git a/api/urls.py b/api/urls.py index 0455b4b..e35f48d 100644 --- a/api/urls.py +++ b/api/urls.py @@ -20,15 +20,23 @@ from rest_framework.routers import DefaultRouter from api import views +REST_FRAMEWORK = { + # Use URL-based versioning + 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning', + 'DEFAULT_VERSION': 'v1', + 'ALLOWED_VERSIONS': {'v1'}, +} + router = DefaultRouter() -router.register(r'playbooks', views.PlaybookViewSet, base_name='playbooks') -router.register(r'plays', views.PlayViewSet, base_name='plays') -router.register(r'tasks', views.TaskViewSet, base_name='tasks') -router.register(r'hosts', views.HostViewSet, base_name='hosts') -router.register(r'results', views.ResultViewSet, base_name='results') -router.register(r'records', views.RecordViewSet, base_name='records') -router.register(r'files', views.FileViewSet, base_name='files') +router.register(r'playbooks', views.PlaybookViewSet) +router.register(r'plays', views.PlayViewSet) +router.register(r'tasks', views.TaskViewSet) +router.register(r'hosts', views.HostViewSet) +router.register(r'results', views.ResultViewSet) +router.register(r'records', views.RecordViewSet) +router.register(r'files', views.FileViewSet) +# router.register(r'filecontent', views.FileContentViewSet) urlpatterns = [ - url(r'^', include(router.urls)), + url(r'^(?P[v1]+)/', include(router.urls)), ] diff --git a/api/views.py b/api/views.py index 2bff5b5..211d7c9 100644 --- a/api/views.py +++ b/api/views.py @@ -50,6 +50,11 @@ class RecordViewSet(viewsets.ModelViewSet): serializer_class = serializers.RecordSerializer +# class FileContentViewSet(viewsets.ModelViewSet): +# queryset = models.FileContent.objects.all() +# serializer_class = serializers.FileContentSerializer + + class FileViewSet(viewsets.ModelViewSet): queryset = models.File.objects.all() serializer_class = serializers.FileSerializer diff --git a/ara/urls.py b/ara/urls.py index 13efa84..348aac6 100644 --- a/ara/urls.py +++ b/ara/urls.py @@ -7,7 +7,7 @@ admin.site.site_header = 'Administration' admin.site.index_title = 'Administration Ara' routes = [ - url(r'^api/v1/', include('api.urls')), + url(r'^api/', include('api.urls')), url(r'^admin/', admin.site.urls), ] urlpatterns = routes + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)