Add support for recording controller fqdn

The controller fqdn that ran the playbook is now recorded and can be
searched for both in the UI and the CLI.

Fixes: https://github.com/ansible-community/ara/issues/193
Change-Id: I53e8d158fc3b6ba7a16582234aaa2542eab5fcdc
This commit is contained in:
David Moreau Simard 2020-12-03 23:46:06 -05:00
parent a4f21d6e3f
commit 52b201dae2
No known key found for this signature in database
GPG Key ID: 7D4729EC4E64E8B7
11 changed files with 80 additions and 2 deletions

View File

@ -56,6 +56,7 @@ class LabelFilter(BaseFilter):
class PlaybookFilter(DateFilter):
controller = django_filters.CharFilter(field_name="controller", lookup_expr="icontains")
name = django_filters.CharFilter(field_name="name", lookup_expr="icontains")
path = django_filters.CharFilter(field_name="path", lookup_expr="icontains")
status = django_filters.MultipleChoiceFilter(

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.17 on 2020-12-04 04:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0007_add_expired_status'),
]
operations = [
migrations.AddField(
model_name='playbook',
name='controller',
field=models.CharField(default='localhost', max_length=255),
),
]

View File

@ -102,6 +102,7 @@ class Playbook(Duration):
arguments = models.BinaryField(max_length=(2 ** 32) - 1)
path = models.CharField(max_length=255)
labels = models.ManyToManyField(Label)
controller = models.CharField(max_length=255, default="localhost")
def __str__(self):
return "<Playbook %s>" % self.id

View File

@ -43,6 +43,7 @@ class PlaybookFactory(DjangoModelFactory):
class Meta:
model = models.Playbook
controller = "localhost"
name = "test-playbook"
ansible_version = "2.4.0"
status = "running"

View File

@ -32,11 +32,17 @@ class PlaybookTestCase(APITestCase):
def test_playbook_serializer(self):
serializer = serializers.PlaybookSerializer(
data={"name": "serializer-playbook", "ansible_version": "2.4.0", "path": "/path/playbook.yml"}
data={
"controller": "serializer",
"name": "serializer-playbook",
"ansible_version": "2.4.0",
"path": "/path/playbook.yml",
}
)
serializer.is_valid()
playbook = serializer.save()
playbook.refresh_from_db()
self.assertEqual(playbook.controller, "serializer")
self.assertEqual(playbook.name, "serializer-playbook")
self.assertEqual(playbook.ansible_version, "2.4.0")
self.assertEqual(playbook.status, "unknown")
@ -125,6 +131,20 @@ class PlaybookTestCase(APITestCase):
request = self.client.get("/api/v1/playbooks/%s" % playbook.id)
self.assertEqual(playbook.ansible_version, request.data["ansible_version"])
def test_get_playbook_by_controller(self):
playbook = factories.PlaybookFactory(name="playbook1", controller="controller-one")
factories.PlaybookFactory(name="playbook2", controller="controller-two")
# Test exact match
request = self.client.get("/api/v1/playbooks?controller=controller-one")
self.assertEqual(1, len(request.data["results"]))
self.assertEqual(playbook.name, request.data["results"][0]["name"])
self.assertEqual(playbook.controller, request.data["results"][0]["controller"])
# Test partial match
request = self.client.get("/api/v1/playbooks?controller=controller")
self.assertEqual(len(request.data["results"]), 2)
def test_get_playbook_by_name(self):
playbook = factories.PlaybookFactory(name="playbook1")
factories.PlaybookFactory(name="playbook2")

View File

@ -31,6 +31,12 @@ class PlaybookList(Lister):
default=None,
help=("List playbooks matching the provided label"),
)
parser.add_argument(
"--controller",
metavar="<controller>",
default=None,
help=("List playbooks that ran from the provided controller (full or partial)"),
)
parser.add_argument(
"--name",
metavar="<name>",
@ -88,6 +94,9 @@ class PlaybookList(Lister):
if args.label is not None:
query["label"] = args.label
if args.controller is not None:
query["controller"] = args.controller
if args.name is not None:
query["name"] = args.name
@ -118,6 +127,7 @@ class PlaybookList(Lister):
columns = (
"id",
"status",
"controller",
"name",
"path",
"plays",
@ -133,6 +143,7 @@ class PlaybookList(Lister):
columns = (
"id",
"status",
"controller",
"path",
"tasks",
"results",
@ -191,6 +202,7 @@ class PlaybookShow(ShowOne):
columns = (
"id",
"report",
"controller",
"status",
"path",
"started",
@ -262,6 +274,12 @@ class PlaybookPrune(Command):
default=None,
help=("Only delete playbooks matching the provided name (full or partial)"),
)
parser.add_argument(
"--controller",
metavar="<controller>",
default=None,
help=("Only delete playbooks that ran from the provided controller (full or partial)"),
)
parser.add_argument(
"--path",
metavar="<path>",
@ -316,6 +334,9 @@ class PlaybookPrune(Command):
if args.label is not None:
query["label"] = args.label
if args.controller is not None:
query["controller"] = args.controller
if args.name is not None:
query["name"] = args.name

View File

@ -21,6 +21,7 @@ import datetime
import json
import logging
import os
import socket
from concurrent.futures import ThreadPoolExecutor
from ansible import __version__ as ansible_version
@ -292,6 +293,7 @@ class CallbackModule(CallbackBase):
arguments=cli_options,
status="running",
path=path,
controller=socket.getfqdn(),
started=datetime.datetime.now(datetime.timezone.utc).isoformat(),
)

View File

@ -21,6 +21,7 @@ from ara.api import models
class PlaybookSearchForm(forms.Form):
controller = forms.CharField(label="Playbook controller", max_length=255, required=False)
name = forms.CharField(label="Playbook name", max_length=255, required=False)
path = forms.CharField(label="Playbook path", max_length=255, required=False)
status = forms.MultipleChoiceField(

View File

@ -7,6 +7,14 @@
<div class="pf-l-flex">
<form novalidate action="/" method="get" class="pf-c-form">
<div class="pf-c-form__group pf-m-inline">
<div class="pf-l-flex__item pf-m-flex-1">
<label class="pf-c-form__label" for="controller">
<span class="pf-c-form__label-text">Controller</span>
</label>
<div class="pf-c-form__horizontal-group">
<input class="pf-c-form-control" type="text" id="controller" name="controller" value="{% if search_form.controller.value is not null %}{{ search_form.controller.value }}{% endif %}" />
</div>
</div>
<div class="pf-l-flex__item pf-m-flex-1">
<label class="pf-c-form__label" for="name">
<span class="pf-c-form__label-text">Name</span>
@ -102,6 +110,7 @@
{% include "partials/sort_by_duration.html" %}
</th>
<th role="columnheader" scope="col" class="pf-m-fit-content">Ansible version</th>
<th role="columnheader" scope="col">Controller</th>
<th role="columnheader" scope="col">Name (or path)</th>
<th role="columnheader" scope="col">Labels</th>
<th role="columnheader" scope="col" class="pf-m-fit-content">Hosts</th>
@ -142,6 +151,9 @@
<td role="cell" data-label="Ansible version" class="pf-m-fit-content">
{{ playbook.ansible_version }}
</td>
<td role="cell" data-label="Controller" class="pf-m-fit-content">
{{ playbook.controller }}
</td>
<td role="cell" data-label="Name (or path)" class="pf-m-fit-content">
{% if static_generation %}
<a href="{% if page != "index" %}../{% endif %}playbooks/{{ playbook.id }}.html" title="{{ playbook.path }}">

View File

@ -22,7 +22,7 @@ class Index(generics.ListAPIView):
def get(self, request, *args, **kwargs):
# TODO: Can we retrieve those fields automatically ?
fields = ["order", "name", "started_after", "status", "label"]
fields = ["order", "controller", "name", "started_after", "status", "label"]
search_query = False
for field in fields:
if field in request.GET:

View File

@ -47,6 +47,7 @@
ansible_version: "9.0.0.1"
started: "{{ ansible_date_time.iso8601_micro }}"
status: running
controller: localhost
labels:
- "{{ _get_root.json['version'] }}"
- "{{ item.name }}:{{ item.tag }}"