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:
parent
a4f21d6e3f
commit
52b201dae2
|
@ -56,6 +56,7 @@ class LabelFilter(BaseFilter):
|
||||||
|
|
||||||
|
|
||||||
class PlaybookFilter(DateFilter):
|
class PlaybookFilter(DateFilter):
|
||||||
|
controller = django_filters.CharFilter(field_name="controller", lookup_expr="icontains")
|
||||||
name = django_filters.CharFilter(field_name="name", lookup_expr="icontains")
|
name = django_filters.CharFilter(field_name="name", lookup_expr="icontains")
|
||||||
path = django_filters.CharFilter(field_name="path", lookup_expr="icontains")
|
path = django_filters.CharFilter(field_name="path", lookup_expr="icontains")
|
||||||
status = django_filters.MultipleChoiceFilter(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -102,6 +102,7 @@ class Playbook(Duration):
|
||||||
arguments = models.BinaryField(max_length=(2 ** 32) - 1)
|
arguments = models.BinaryField(max_length=(2 ** 32) - 1)
|
||||||
path = models.CharField(max_length=255)
|
path = models.CharField(max_length=255)
|
||||||
labels = models.ManyToManyField(Label)
|
labels = models.ManyToManyField(Label)
|
||||||
|
controller = models.CharField(max_length=255, default="localhost")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "<Playbook %s>" % self.id
|
return "<Playbook %s>" % self.id
|
||||||
|
|
|
@ -43,6 +43,7 @@ class PlaybookFactory(DjangoModelFactory):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Playbook
|
model = models.Playbook
|
||||||
|
|
||||||
|
controller = "localhost"
|
||||||
name = "test-playbook"
|
name = "test-playbook"
|
||||||
ansible_version = "2.4.0"
|
ansible_version = "2.4.0"
|
||||||
status = "running"
|
status = "running"
|
||||||
|
|
|
@ -32,11 +32,17 @@ class PlaybookTestCase(APITestCase):
|
||||||
|
|
||||||
def test_playbook_serializer(self):
|
def test_playbook_serializer(self):
|
||||||
serializer = serializers.PlaybookSerializer(
|
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()
|
serializer.is_valid()
|
||||||
playbook = serializer.save()
|
playbook = serializer.save()
|
||||||
playbook.refresh_from_db()
|
playbook.refresh_from_db()
|
||||||
|
self.assertEqual(playbook.controller, "serializer")
|
||||||
self.assertEqual(playbook.name, "serializer-playbook")
|
self.assertEqual(playbook.name, "serializer-playbook")
|
||||||
self.assertEqual(playbook.ansible_version, "2.4.0")
|
self.assertEqual(playbook.ansible_version, "2.4.0")
|
||||||
self.assertEqual(playbook.status, "unknown")
|
self.assertEqual(playbook.status, "unknown")
|
||||||
|
@ -125,6 +131,20 @@ class PlaybookTestCase(APITestCase):
|
||||||
request = self.client.get("/api/v1/playbooks/%s" % playbook.id)
|
request = self.client.get("/api/v1/playbooks/%s" % playbook.id)
|
||||||
self.assertEqual(playbook.ansible_version, request.data["ansible_version"])
|
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):
|
def test_get_playbook_by_name(self):
|
||||||
playbook = factories.PlaybookFactory(name="playbook1")
|
playbook = factories.PlaybookFactory(name="playbook1")
|
||||||
factories.PlaybookFactory(name="playbook2")
|
factories.PlaybookFactory(name="playbook2")
|
||||||
|
|
|
@ -31,6 +31,12 @@ class PlaybookList(Lister):
|
||||||
default=None,
|
default=None,
|
||||||
help=("List playbooks matching the provided label"),
|
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(
|
parser.add_argument(
|
||||||
"--name",
|
"--name",
|
||||||
metavar="<name>",
|
metavar="<name>",
|
||||||
|
@ -88,6 +94,9 @@ class PlaybookList(Lister):
|
||||||
if args.label is not None:
|
if args.label is not None:
|
||||||
query["label"] = args.label
|
query["label"] = args.label
|
||||||
|
|
||||||
|
if args.controller is not None:
|
||||||
|
query["controller"] = args.controller
|
||||||
|
|
||||||
if args.name is not None:
|
if args.name is not None:
|
||||||
query["name"] = args.name
|
query["name"] = args.name
|
||||||
|
|
||||||
|
@ -118,6 +127,7 @@ class PlaybookList(Lister):
|
||||||
columns = (
|
columns = (
|
||||||
"id",
|
"id",
|
||||||
"status",
|
"status",
|
||||||
|
"controller",
|
||||||
"name",
|
"name",
|
||||||
"path",
|
"path",
|
||||||
"plays",
|
"plays",
|
||||||
|
@ -133,6 +143,7 @@ class PlaybookList(Lister):
|
||||||
columns = (
|
columns = (
|
||||||
"id",
|
"id",
|
||||||
"status",
|
"status",
|
||||||
|
"controller",
|
||||||
"path",
|
"path",
|
||||||
"tasks",
|
"tasks",
|
||||||
"results",
|
"results",
|
||||||
|
@ -191,6 +202,7 @@ class PlaybookShow(ShowOne):
|
||||||
columns = (
|
columns = (
|
||||||
"id",
|
"id",
|
||||||
"report",
|
"report",
|
||||||
|
"controller",
|
||||||
"status",
|
"status",
|
||||||
"path",
|
"path",
|
||||||
"started",
|
"started",
|
||||||
|
@ -262,6 +274,12 @@ class PlaybookPrune(Command):
|
||||||
default=None,
|
default=None,
|
||||||
help=("Only delete playbooks matching the provided name (full or partial)"),
|
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(
|
parser.add_argument(
|
||||||
"--path",
|
"--path",
|
||||||
metavar="<path>",
|
metavar="<path>",
|
||||||
|
@ -316,6 +334,9 @@ class PlaybookPrune(Command):
|
||||||
if args.label is not None:
|
if args.label is not None:
|
||||||
query["label"] = args.label
|
query["label"] = args.label
|
||||||
|
|
||||||
|
if args.controller is not None:
|
||||||
|
query["controller"] = args.controller
|
||||||
|
|
||||||
if args.name is not None:
|
if args.name is not None:
|
||||||
query["name"] = args.name
|
query["name"] = args.name
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import datetime
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import socket
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
from ansible import __version__ as ansible_version
|
from ansible import __version__ as ansible_version
|
||||||
|
@ -292,6 +293,7 @@ class CallbackModule(CallbackBase):
|
||||||
arguments=cli_options,
|
arguments=cli_options,
|
||||||
status="running",
|
status="running",
|
||||||
path=path,
|
path=path,
|
||||||
|
controller=socket.getfqdn(),
|
||||||
started=datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
started=datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ from ara.api import models
|
||||||
|
|
||||||
|
|
||||||
class PlaybookSearchForm(forms.Form):
|
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)
|
name = forms.CharField(label="Playbook name", max_length=255, required=False)
|
||||||
path = forms.CharField(label="Playbook path", max_length=255, required=False)
|
path = forms.CharField(label="Playbook path", max_length=255, required=False)
|
||||||
status = forms.MultipleChoiceField(
|
status = forms.MultipleChoiceField(
|
||||||
|
|
|
@ -7,6 +7,14 @@
|
||||||
<div class="pf-l-flex">
|
<div class="pf-l-flex">
|
||||||
<form novalidate action="/" method="get" class="pf-c-form">
|
<form novalidate action="/" method="get" class="pf-c-form">
|
||||||
<div class="pf-c-form__group pf-m-inline">
|
<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">
|
<div class="pf-l-flex__item pf-m-flex-1">
|
||||||
<label class="pf-c-form__label" for="name">
|
<label class="pf-c-form__label" for="name">
|
||||||
<span class="pf-c-form__label-text">Name</span>
|
<span class="pf-c-form__label-text">Name</span>
|
||||||
|
@ -102,6 +110,7 @@
|
||||||
{% include "partials/sort_by_duration.html" %}
|
{% include "partials/sort_by_duration.html" %}
|
||||||
</th>
|
</th>
|
||||||
<th role="columnheader" scope="col" class="pf-m-fit-content">Ansible version</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">Name (or path)</th>
|
||||||
<th role="columnheader" scope="col">Labels</th>
|
<th role="columnheader" scope="col">Labels</th>
|
||||||
<th role="columnheader" scope="col" class="pf-m-fit-content">Hosts</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">
|
<td role="cell" data-label="Ansible version" class="pf-m-fit-content">
|
||||||
{{ playbook.ansible_version }}
|
{{ playbook.ansible_version }}
|
||||||
</td>
|
</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">
|
<td role="cell" data-label="Name (or path)" class="pf-m-fit-content">
|
||||||
{% if static_generation %}
|
{% if static_generation %}
|
||||||
<a href="{% if page != "index" %}../{% endif %}playbooks/{{ playbook.id }}.html" title="{{ playbook.path }}">
|
<a href="{% if page != "index" %}../{% endif %}playbooks/{{ playbook.id }}.html" title="{{ playbook.path }}">
|
||||||
|
|
|
@ -22,7 +22,7 @@ class Index(generics.ListAPIView):
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
# TODO: Can we retrieve those fields automatically ?
|
# 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
|
search_query = False
|
||||||
for field in fields:
|
for field in fields:
|
||||||
if field in request.GET:
|
if field in request.GET:
|
||||||
|
|
|
@ -47,6 +47,7 @@
|
||||||
ansible_version: "9.0.0.1"
|
ansible_version: "9.0.0.1"
|
||||||
started: "{{ ansible_date_time.iso8601_micro }}"
|
started: "{{ ansible_date_time.iso8601_micro }}"
|
||||||
status: running
|
status: running
|
||||||
|
controller: localhost
|
||||||
labels:
|
labels:
|
||||||
- "{{ _get_root.json['version'] }}"
|
- "{{ _get_root.json['version'] }}"
|
||||||
- "{{ item.name }}:{{ item.tag }}"
|
- "{{ item.name }}:{{ item.tag }}"
|
||||||
|
|
Loading…
Reference in New Issue