From 52b201dae2c99f3f9b5fd015a7e3a81f81ed5440 Mon Sep 17 00:00:00 2001 From: David Moreau Simard Date: Thu, 3 Dec 2020 23:46:06 -0500 Subject: [PATCH] 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 --- ara/api/filters.py | 1 + .../migrations/0008_playbook_controller.py | 18 +++++++++++++++ ara/api/models.py | 1 + ara/api/tests/factories.py | 1 + ara/api/tests/tests_playbook.py | 22 ++++++++++++++++++- ara/cli/playbook.py | 21 ++++++++++++++++++ ara/plugins/callback/ara_default.py | 2 ++ ara/ui/forms.py | 1 + ara/ui/templates/index.html | 12 ++++++++++ ara/ui/views.py | 2 +- tests/container_test_tasks.yaml | 1 + 11 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 ara/api/migrations/0008_playbook_controller.py diff --git a/ara/api/filters.py b/ara/api/filters.py index c0f42430..6ced353b 100644 --- a/ara/api/filters.py +++ b/ara/api/filters.py @@ -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( diff --git a/ara/api/migrations/0008_playbook_controller.py b/ara/api/migrations/0008_playbook_controller.py new file mode 100644 index 00000000..a46ed712 --- /dev/null +++ b/ara/api/migrations/0008_playbook_controller.py @@ -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), + ), + ] diff --git a/ara/api/models.py b/ara/api/models.py index 703245b0..75d20434 100644 --- a/ara/api/models.py +++ b/ara/api/models.py @@ -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 "" % self.id diff --git a/ara/api/tests/factories.py b/ara/api/tests/factories.py index 7fcf753d..7f9397e4 100644 --- a/ara/api/tests/factories.py +++ b/ara/api/tests/factories.py @@ -43,6 +43,7 @@ class PlaybookFactory(DjangoModelFactory): class Meta: model = models.Playbook + controller = "localhost" name = "test-playbook" ansible_version = "2.4.0" status = "running" diff --git a/ara/api/tests/tests_playbook.py b/ara/api/tests/tests_playbook.py index f23496e1..ee92e62c 100644 --- a/ara/api/tests/tests_playbook.py +++ b/ara/api/tests/tests_playbook.py @@ -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") diff --git a/ara/cli/playbook.py b/ara/cli/playbook.py index 962b2aac..83299184 100644 --- a/ara/cli/playbook.py +++ b/ara/cli/playbook.py @@ -31,6 +31,12 @@ class PlaybookList(Lister): default=None, help=("List playbooks matching the provided label"), ) + parser.add_argument( + "--controller", + metavar="", + default=None, + help=("List playbooks that ran from the provided controller (full or partial)"), + ) parser.add_argument( "--name", metavar="", @@ -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="", + default=None, + help=("Only delete playbooks that ran from the provided controller (full or partial)"), + ) parser.add_argument( "--path", metavar="", @@ -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 diff --git a/ara/plugins/callback/ara_default.py b/ara/plugins/callback/ara_default.py index bbbaf3c9..8981d0da 100644 --- a/ara/plugins/callback/ara_default.py +++ b/ara/plugins/callback/ara_default.py @@ -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(), ) diff --git a/ara/ui/forms.py b/ara/ui/forms.py index 5b7af91e..0f3126ff 100644 --- a/ara/ui/forms.py +++ b/ara/ui/forms.py @@ -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( diff --git a/ara/ui/templates/index.html b/ara/ui/templates/index.html index d4da9fad..58c4010e 100644 --- a/ara/ui/templates/index.html +++ b/ara/ui/templates/index.html @@ -7,6 +7,14 @@