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 "<Playbook %s>" % 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="<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 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 @@ <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 }}"> diff --git a/ara/ui/views.py b/ara/ui/views.py index 825f8290..f21b0368 100644 --- a/ara/ui/views.py +++ b/ara/ui/views.py @@ -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: diff --git a/tests/container_test_tasks.yaml b/tests/container_test_tasks.yaml index 148f0855..cd2e8e5d 100644 --- a/tests/container_test_tasks.yaml +++ b/tests/container_test_tasks.yaml @@ -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 }}"