API: Stop returning nested children resources
When querying the API for a playbook's detail, it would return all of it's children (hosts, files, tasks, results) which could be very slow when dealing with larger playbooks. We no longer do that for playbooks as well as plays and tasks. Instead, we can easily find a playbook's resources by searching for them with the playbook id like so: - /api/v1/plays?playbook=<id> - /api/v1/tasks?playbook=<id> - /api/v1/results?playbook=<id> ... and so on. This commit adapts the built-in UI because it would've otherwise been broken by the change. Fixes: https://github.com/ansible-community/ara/issues/158 Change-Id: I442bff657e5da9d6a3916ebdbc5e66c0e670b00f
This commit is contained in:
parent
e2534a10fd
commit
74defc2273
|
@ -74,8 +74,7 @@ class FileSha1Serializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
#######
|
#######
|
||||||
# Simple serializers provide lightweight representations of objects without
|
# Simple serializers provide lightweight representations of objects suitable for inclusion in other objects
|
||||||
# nested or large fields.
|
|
||||||
#######
|
#######
|
||||||
|
|
||||||
|
|
||||||
|
@ -96,108 +95,27 @@ class SimplePlaybookSerializer(ItemCountSerializer):
|
||||||
class SimplePlaySerializer(ItemCountSerializer):
|
class SimplePlaySerializer(ItemCountSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Play
|
model = models.Play
|
||||||
exclude = ("uuid", "created", "updated")
|
exclude = ("playbook", "uuid", "created", "updated")
|
||||||
|
|
||||||
|
|
||||||
class SimpleTaskSerializer(ItemCountSerializer, TaskPathSerializer):
|
class SimpleTaskSerializer(ItemCountSerializer, TaskPathSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Task
|
model = models.Task
|
||||||
exclude = ("tags", "created", "updated")
|
exclude = ("playbook", "play", "created", "updated")
|
||||||
|
|
||||||
|
tags = ara_fields.CompressedObjectField(read_only=True)
|
||||||
class SimpleResultSerializer(ResultStatusSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = models.Result
|
|
||||||
exclude = ("content", "created", "updated")
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleHostSerializer(serializers.ModelSerializer):
|
class SimpleHostSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Host
|
model = models.Host
|
||||||
exclude = ("facts", "created", "updated")
|
exclude = ("playbook", "facts", "created", "updated")
|
||||||
|
|
||||||
|
|
||||||
class SimpleFileSerializer(FileSha1Serializer):
|
class SimpleFileSerializer(FileSha1Serializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.File
|
model = models.File
|
||||||
exclude = ("content", "created", "updated")
|
exclude = ("playbook", "content", "created", "updated")
|
||||||
|
|
||||||
|
|
||||||
class SimpleRecordSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = models.Record
|
|
||||||
exclude = ("value", "created", "updated")
|
|
||||||
|
|
||||||
|
|
||||||
#######
|
|
||||||
# Nested serializers returns optimized data within the context of another object.
|
|
||||||
# For example: when retrieving a playbook, we'll already have the playbook id
|
|
||||||
# so it is not necessary to include it in nested objects.
|
|
||||||
#######
|
|
||||||
|
|
||||||
|
|
||||||
class NestedPlaybookFileSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = models.File
|
|
||||||
exclude = ("content", "created", "updated", "playbook")
|
|
||||||
|
|
||||||
|
|
||||||
class NestedPlaybookHostSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = models.Host
|
|
||||||
fields = ("id", "name")
|
|
||||||
|
|
||||||
|
|
||||||
class NestedPlaybookResultSerializer(ResultStatusSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = models.Result
|
|
||||||
exclude = ("content", "created", "updated", "playbook", "play", "task")
|
|
||||||
|
|
||||||
host = NestedPlaybookHostSerializer(read_only=True)
|
|
||||||
|
|
||||||
|
|
||||||
class NestedPlaybookTaskSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = models.Task
|
|
||||||
exclude = ("playbook", "created", "updated")
|
|
||||||
|
|
||||||
tags = ara_fields.CompressedObjectField(read_only=True)
|
|
||||||
file = NestedPlaybookFileSerializer(read_only=True)
|
|
||||||
results = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_results(obj):
|
|
||||||
results = obj.results.all().order_by("-id")
|
|
||||||
return NestedPlaybookResultSerializer(results, many=True).data
|
|
||||||
|
|
||||||
|
|
||||||
class NestedPlaybookRecordSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = models.Record
|
|
||||||
exclude = ("playbook", "value", "created", "updated")
|
|
||||||
|
|
||||||
|
|
||||||
class NestedPlaybookPlaySerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = models.Play
|
|
||||||
exclude = ("playbook", "uuid", "created", "updated")
|
|
||||||
|
|
||||||
tasks = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_tasks(obj):
|
|
||||||
tasks = obj.tasks.all().order_by("-id")
|
|
||||||
return NestedPlaybookTaskSerializer(tasks, many=True).data
|
|
||||||
|
|
||||||
|
|
||||||
class NestedPlayTaskSerializer(TaskPathSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = models.Task
|
|
||||||
exclude = ("playbook", "play", "created", "updated")
|
|
||||||
|
|
||||||
tags = ara_fields.CompressedObjectField(read_only=True)
|
|
||||||
results = NestedPlaybookResultSerializer(read_only=True, many=True)
|
|
||||||
file = NestedPlaybookFileSerializer(read_only=True)
|
|
||||||
|
|
||||||
|
|
||||||
#######
|
#######
|
||||||
|
@ -219,15 +137,6 @@ class DetailedPlaybookSerializer(ItemCountSerializer):
|
||||||
|
|
||||||
arguments = ara_fields.CompressedObjectField(default=ara_fields.EMPTY_DICT, read_only=True)
|
arguments = ara_fields.CompressedObjectField(default=ara_fields.EMPTY_DICT, read_only=True)
|
||||||
labels = SimpleLabelSerializer(many=True, read_only=True, default=[])
|
labels = SimpleLabelSerializer(many=True, read_only=True, default=[])
|
||||||
hosts = SimpleHostSerializer(many=True, read_only=True, default=[])
|
|
||||||
files = SimpleFileSerializer(many=True, read_only=True, default=[])
|
|
||||||
records = NestedPlaybookRecordSerializer(many=True, read_only=True, default=[])
|
|
||||||
plays = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_plays(obj):
|
|
||||||
plays = obj.plays.all().order_by("-id")
|
|
||||||
return NestedPlaybookPlaySerializer(plays, many=True).data
|
|
||||||
|
|
||||||
|
|
||||||
class DetailedPlaySerializer(ItemCountSerializer):
|
class DetailedPlaySerializer(ItemCountSerializer):
|
||||||
|
@ -236,7 +145,6 @@ class DetailedPlaySerializer(ItemCountSerializer):
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
playbook = SimplePlaybookSerializer(read_only=True)
|
playbook = SimplePlaybookSerializer(read_only=True)
|
||||||
tasks = NestedPlayTaskSerializer(many=True, read_only=True, default=[])
|
|
||||||
|
|
||||||
|
|
||||||
class DetailedTaskSerializer(ItemCountSerializer, TaskPathSerializer):
|
class DetailedTaskSerializer(ItemCountSerializer, TaskPathSerializer):
|
||||||
|
@ -247,7 +155,6 @@ class DetailedTaskSerializer(ItemCountSerializer, TaskPathSerializer):
|
||||||
playbook = SimplePlaybookSerializer(read_only=True)
|
playbook = SimplePlaybookSerializer(read_only=True)
|
||||||
play = SimplePlaySerializer(read_only=True)
|
play = SimplePlaySerializer(read_only=True)
|
||||||
file = SimpleFileSerializer(read_only=True)
|
file = SimpleFileSerializer(read_only=True)
|
||||||
results = NestedPlaybookResultSerializer(many=True, read_only=True, default=[])
|
|
||||||
tags = ara_fields.CompressedObjectField(read_only=True)
|
tags = ara_fields.CompressedObjectField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<div class="pf-c-card__body">
|
<div class="pf-c-card__body">
|
||||||
<details id="records">
|
<details id="records">
|
||||||
<summary>Records</summary>
|
<summary>Records</summary>
|
||||||
{% if playbook.items.records %}
|
{% if records %}
|
||||||
<ul class="pf-c-list">
|
<ul class="pf-c-list">
|
||||||
{% for record in playbook.records %}
|
{% for record in playbook.records %}
|
||||||
<li><a href="../records/{{ record.id }}.html">{{ record.key }}</a></li>
|
<li><a href="../records/{{ record.id }}.html">{{ record.key }}</a></li>
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
<details id="files">
|
<details id="files">
|
||||||
<summary>Files</summary>
|
<summary>Files</summary>
|
||||||
<ul class="pf-c-list">
|
<ul class="pf-c-list">
|
||||||
{% for file in playbook.files %}
|
{% for file in files %}
|
||||||
<li><a href="../files/{{ file.id }}.html">{{ file.path }}</a></li>
|
<li><a href="../files/{{ file.id }}.html">{{ file.path }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -74,7 +74,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for host in playbook.hosts %}
|
{% for host in hosts %}
|
||||||
<tr>
|
<tr>
|
||||||
<td role="cell" data-label="Hostname" class="pf-m-fit-content">
|
<td role="cell" data-label="Hostname" class="pf-m-fit-content">
|
||||||
<a href="../hosts/{{ host.id }}.html">{{ host.name }}</a>
|
<a href="../hosts/{{ host.id }}.html">{{ host.name }}</a>
|
||||||
|
@ -117,9 +117,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for play in playbook.plays %}
|
{% for result in results %}
|
||||||
{% for task in play.tasks %}
|
|
||||||
{% for result in task.results %}
|
|
||||||
<tr role="row">
|
<tr role="row">
|
||||||
<td role="cell" data-label="Status" class="pf-c-table__icon pf-m-fit-content">
|
<td role="cell" data-label="Status" class="pf-c-table__icon pf-m-fit-content">
|
||||||
{% include "partials/result_status_icon.html" with status=result.status %}
|
{% include "partials/result_status_icon.html" with status=result.status %}
|
||||||
|
@ -128,10 +126,10 @@
|
||||||
{{ result.host.name }}
|
{{ result.host.name }}
|
||||||
</td>
|
</td>
|
||||||
<td role="cell" data-label="Name" class="pf-m-fit-content">
|
<td role="cell" data-label="Name" class="pf-m-fit-content">
|
||||||
<a href="../results/{{ result.id }}.html">{{ task.name }}</a>
|
<a href="../results/{{ result.id }}.html">{{ result.task.name }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td role="cell" data-label="Action" class="pf-m-fit-content">
|
<td role="cell" data-label="Action" class="pf-m-fit-content">
|
||||||
<a href="../files/{{ task.file.id }}.html#line-{{ task.lineno }}">{{ task.action }}</a>
|
<a href="../files/{{ task.file.id }}.html#line-{{ result.task.lineno }}">{{ result.task.action }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td role="cell" data-label="Duration" class="pf-m-fit-content">
|
<td role="cell" data-label="Duration" class="pf-m-fit-content">
|
||||||
{{ result.duration | format_duration }}
|
{{ result.duration | format_duration }}
|
||||||
|
@ -141,8 +139,6 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</details>
|
</details>
|
||||||
|
|
|
@ -68,9 +68,35 @@ class Playbook(generics.RetrieveAPIView):
|
||||||
template_name = "playbook.html"
|
template_name = "playbook.html"
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
playbook = self.get_object()
|
playbook = serializers.DetailedPlaybookSerializer(self.get_object())
|
||||||
serializer = serializers.DetailedPlaybookSerializer(playbook)
|
hosts = serializers.ListHostSerializer(
|
||||||
return Response({"playbook": serializer.data})
|
models.Host.objects.filter(playbook=playbook.data["id"]).all(), many=True
|
||||||
|
)
|
||||||
|
files = serializers.ListFileSerializer(
|
||||||
|
models.File.objects.filter(playbook=playbook.data["id"]).all(), many=True
|
||||||
|
)
|
||||||
|
records = serializers.ListRecordSerializer(
|
||||||
|
models.Record.objects.filter(playbook=playbook.data["id"]).all(), many=True
|
||||||
|
)
|
||||||
|
results = serializers.ListResultSerializer(
|
||||||
|
models.Result.objects.filter(playbook=playbook.data["id"]).all(), many=True
|
||||||
|
)
|
||||||
|
|
||||||
|
for result in results.data:
|
||||||
|
task_id = result["task"]
|
||||||
|
result["task"] = serializers.SimpleTaskSerializer(models.Task.objects.get(pk=task_id)).data
|
||||||
|
host_id = result["host"]
|
||||||
|
result["host"] = serializers.SimpleHostSerializer(models.Host.objects.get(pk=host_id)).data
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
return Response({
|
||||||
|
"playbook": playbook.data,
|
||||||
|
"hosts": hosts.data,
|
||||||
|
"files": files.data,
|
||||||
|
"records": records.data,
|
||||||
|
"results": results.data
|
||||||
|
})
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
class Host(generics.RetrieveAPIView):
|
class Host(generics.RetrieveAPIView):
|
||||||
|
|
|
@ -82,25 +82,30 @@ Here's a code example to help you get started:
|
||||||
# If there are any results from our query, get more information about the
|
# If there are any results from our query, get more information about the
|
||||||
# failure and print something helpful
|
# failure and print something helpful
|
||||||
template = "{timestamp}: {host} failed '{task}' ({task_file}:{lineno})"
|
template = "{timestamp}: {host} failed '{task}' ({task_file}:{lineno})"
|
||||||
for playbook in playbooks["results"]:
|
|
||||||
# Get a detailed version of the playbook that provides additional context
|
|
||||||
detailed_playbook = client.get("/api/v1/playbooks/%s" % playbook["id"])
|
|
||||||
|
|
||||||
# Iterate through the playbook to get the context
|
for playbook in playbooks["results"]:
|
||||||
# Playbook -> Play -> Task -> Result <- Host
|
# Get failed results for the playbook
|
||||||
for play in detailed_playbook["plays"]:
|
results = client.get("/api/v1/results?playbook=%s" % playbook["id"])
|
||||||
for task in play["tasks"]:
|
|
||||||
for result in task["results"]:
|
# For each result, print the task and host information
|
||||||
if result["status"] in ["failed", "unreachable"]:
|
for result in results["results"]:
|
||||||
print(template.format(
|
task = client.get("/api/v1/tasks/%s" % result["task"])
|
||||||
timestamp=result["ended"],
|
host = client.get("/api/v1/hosts/%s" % result["host"])
|
||||||
host=result["host"]["name"],
|
|
||||||
task=task["name"],
|
print(template.format(
|
||||||
task_file=task["file"]["path"],
|
timestamp=result["ended"],
|
||||||
lineno=task["lineno"]
|
host=host["name"],
|
||||||
))
|
task=task["name"],
|
||||||
|
task_file=task["path"],
|
||||||
|
lineno=task["lineno"]
|
||||||
|
))
|
||||||
|
|
||||||
Running this script would then provide an output that looks like the following::
|
Running this script would then provide an output that looks like the following::
|
||||||
|
|
||||||
2019-03-20T16:18:41.710765: localhost failed 'smoke-tests : Return false' (tests/integration/roles/smoke-tests/tasks/test-ops.yaml:25)
|
2020-04-18T17:16:13.394056Z: aio1_repo_container-0c92f7a2 failed 'repo_server : Install EPEL gpg keys' (/home/zuul/src/opendev.org/openstack/openstack-ansible-repo_server/tasks/repo_install.yml:16)
|
||||||
2019-03-20T16:19:17.332663: localhost failed 'fail' (tests/integration/failed.yaml:22)
|
2020-04-18T17:14:59.930995Z: aio1_repo_container-0c92f7a2 failed 'repo_server : File and directory setup (root user)' (/home/zuul/src/opendev.org/openstack/openstack-ansible-repo_server/tasks/repo_pre_install.yml:78)
|
||||||
|
2020-04-18T17:14:57.909155Z: aio1_repo_container-0c92f7a2 failed 'repo_server : Git service data folder setup' (/home/zuul/src/opendev.org/openstack/openstack-ansible-repo_server/tasks/repo_pre_install.yml:70)
|
||||||
|
2020-04-18T17:14:57.342091Z: aio1_repo_container-0c92f7a2 failed 'repo_server : Check if the git folder exists already' (/home/zuul/src/opendev.org/openstack/openstack-ansible-repo_server/tasks/repo_pre_install.yml:65)
|
||||||
|
2020-04-18T17:14:56.793499Z: aio1_repo_container-0c92f7a2 failed 'repo_server : Drop repo pre/post command script' (/home/zuul/src/opendev.org/openstack/openstack-ansible-repo_server/tasks/repo_pre_install.yml:53)
|
||||||
|
2020-04-18T17:14:54.507660Z: aio1_repo_container-0c92f7a2 failed 'repo_server : File and directory setup (non-root user)' (/home/zuul/src/opendev.org/openstack/openstack-ansible-repo_server/tasks/repo_pre_install.yml:32)
|
||||||
|
2020-04-18T17:14:51.281530Z: aio1_repo_container-0c92f7a2 failed 'repo_server : Create the nginx system user' (/home/zuul/src/opendev.org/openstack/openstack-ansible-repo_server/tasks/repo_pre_install.yml:22)
|
||||||
|
|
|
@ -26,11 +26,6 @@
|
||||||
- playbook.ansible_version == ansible_version.full
|
- playbook.ansible_version == ansible_version.full
|
||||||
- playbook_dir in playbook.path
|
- playbook_dir in playbook.path
|
||||||
- "'tests/integration/lookups.yaml' in playbook.path"
|
- "'tests/integration/lookups.yaml' in playbook.path"
|
||||||
- "playbook.files | length == playbook['items']['files']"
|
|
||||||
- "playbook.hosts | length == playbook['items']['hosts']"
|
|
||||||
- "playbook.plays | length == playbook['items']['plays']"
|
|
||||||
- "tasks.results | length == playbook['items']['tasks']"
|
|
||||||
- "results.results | length == playbook['items']['results']"
|
|
||||||
|
|
||||||
#####
|
#####
|
||||||
# Examples taken from docs on Ansible plugins and use cases
|
# Examples taken from docs on Ansible plugins and use cases
|
||||||
|
|
Loading…
Reference in New Issue