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
|
||||
# nested or large fields.
|
||||
# Simple serializers provide lightweight representations of objects suitable for inclusion in other objects
|
||||
#######
|
||||
|
||||
|
||||
@ -96,108 +95,27 @@ class SimplePlaybookSerializer(ItemCountSerializer):
|
||||
class SimplePlaySerializer(ItemCountSerializer):
|
||||
class Meta:
|
||||
model = models.Play
|
||||
exclude = ("uuid", "created", "updated")
|
||||
exclude = ("playbook", "uuid", "created", "updated")
|
||||
|
||||
|
||||
class SimpleTaskSerializer(ItemCountSerializer, TaskPathSerializer):
|
||||
class Meta:
|
||||
model = models.Task
|
||||
exclude = ("tags", "created", "updated")
|
||||
exclude = ("playbook", "play", "created", "updated")
|
||||
|
||||
|
||||
class SimpleResultSerializer(ResultStatusSerializer):
|
||||
class Meta:
|
||||
model = models.Result
|
||||
exclude = ("content", "created", "updated")
|
||||
tags = ara_fields.CompressedObjectField(read_only=True)
|
||||
|
||||
|
||||
class SimpleHostSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = models.Host
|
||||
exclude = ("facts", "created", "updated")
|
||||
exclude = ("playbook", "facts", "created", "updated")
|
||||
|
||||
|
||||
class SimpleFileSerializer(FileSha1Serializer):
|
||||
class Meta:
|
||||
model = models.File
|
||||
exclude = ("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)
|
||||
exclude = ("playbook", "content", "created", "updated")
|
||||
|
||||
|
||||
#######
|
||||
@ -219,15 +137,6 @@ class DetailedPlaybookSerializer(ItemCountSerializer):
|
||||
|
||||
arguments = ara_fields.CompressedObjectField(default=ara_fields.EMPTY_DICT, read_only=True)
|
||||
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):
|
||||
@ -236,7 +145,6 @@ class DetailedPlaySerializer(ItemCountSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
playbook = SimplePlaybookSerializer(read_only=True)
|
||||
tasks = NestedPlayTaskSerializer(many=True, read_only=True, default=[])
|
||||
|
||||
|
||||
class DetailedTaskSerializer(ItemCountSerializer, TaskPathSerializer):
|
||||
@ -247,7 +155,6 @@ class DetailedTaskSerializer(ItemCountSerializer, TaskPathSerializer):
|
||||
playbook = SimplePlaybookSerializer(read_only=True)
|
||||
play = SimplePlaySerializer(read_only=True)
|
||||
file = SimpleFileSerializer(read_only=True)
|
||||
results = NestedPlaybookResultSerializer(many=True, read_only=True, default=[])
|
||||
tags = ara_fields.CompressedObjectField(read_only=True)
|
||||
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
<div class="pf-c-card__body">
|
||||
<details id="records">
|
||||
<summary>Records</summary>
|
||||
{% if playbook.items.records %}
|
||||
{% if records %}
|
||||
<ul class="pf-c-list">
|
||||
{% for record in playbook.records %}
|
||||
<li><a href="../records/{{ record.id }}.html">{{ record.key }}</a></li>
|
||||
@ -20,7 +20,7 @@
|
||||
<details id="files">
|
||||
<summary>Files</summary>
|
||||
<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>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@ -74,7 +74,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for host in playbook.hosts %}
|
||||
{% for host in hosts %}
|
||||
<tr>
|
||||
<td role="cell" data-label="Hostname" class="pf-m-fit-content">
|
||||
<a href="../hosts/{{ host.id }}.html">{{ host.name }}</a>
|
||||
@ -117,9 +117,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for play in playbook.plays %}
|
||||
{% for task in play.tasks %}
|
||||
{% for result in task.results %}
|
||||
{% for result in results %}
|
||||
<tr role="row">
|
||||
<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 %}
|
||||
@ -128,10 +126,10 @@
|
||||
{{ result.host.name }}
|
||||
</td>
|
||||
<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 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 role="cell" data-label="Duration" class="pf-m-fit-content">
|
||||
{{ result.duration | format_duration }}
|
||||
@ -141,8 +139,6 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
|
@ -68,9 +68,35 @@ class Playbook(generics.RetrieveAPIView):
|
||||
template_name = "playbook.html"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
playbook = self.get_object()
|
||||
serializer = serializers.DetailedPlaybookSerializer(playbook)
|
||||
return Response({"playbook": serializer.data})
|
||||
playbook = serializers.DetailedPlaybookSerializer(self.get_object())
|
||||
hosts = serializers.ListHostSerializer(
|
||||
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):
|
||||
|
@ -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
|
||||
# failure and print something helpful
|
||||
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
|
||||
# Playbook -> Play -> Task -> Result <- Host
|
||||
for play in detailed_playbook["plays"]:
|
||||
for task in play["tasks"]:
|
||||
for result in task["results"]:
|
||||
if result["status"] in ["failed", "unreachable"]:
|
||||
print(template.format(
|
||||
timestamp=result["ended"],
|
||||
host=result["host"]["name"],
|
||||
task=task["name"],
|
||||
task_file=task["file"]["path"],
|
||||
lineno=task["lineno"]
|
||||
))
|
||||
for playbook in playbooks["results"]:
|
||||
# Get failed results for the playbook
|
||||
results = client.get("/api/v1/results?playbook=%s" % playbook["id"])
|
||||
|
||||
# For each result, print the task and host information
|
||||
for result in results["results"]:
|
||||
task = client.get("/api/v1/tasks/%s" % result["task"])
|
||||
host = client.get("/api/v1/hosts/%s" % result["host"])
|
||||
|
||||
print(template.format(
|
||||
timestamp=result["ended"],
|
||||
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::
|
||||
|
||||
2019-03-20T16:18:41.710765: localhost failed 'smoke-tests : Return false' (tests/integration/roles/smoke-tests/tasks/test-ops.yaml:25)
|
||||
2019-03-20T16:19:17.332663: localhost failed 'fail' (tests/integration/failed.yaml:22)
|
||||
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)
|
||||
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_dir 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
|
||||
|
Loading…
x
Reference in New Issue
Block a user