Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bulk operations for processes #101

Merged
merged 6 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions main/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,4 @@ class ProcessTable(tables.Table):
status_code = tables.Column(verbose_name="Status Code")
exit_code = tables.Column(verbose_name="Exit Code")
logs = tables.TemplateColumn(logs_column_template, verbose_name="Logs")
restart = tables.TemplateColumn(restart_column_template, verbose_name="Restart")
flush = tables.TemplateColumn(flush_column_template, verbose_name="Flush")
kill = tables.TemplateColumn(kill_column_template, verbose_name="Kill")
select = tables.CheckBoxColumn(accessor="uuid", verbose_name="Select")
12 changes: 10 additions & 2 deletions main/templates/main/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
{% block content %}

<div class="col">

{% if messages %}
<h2>Messages</h2>
<div style="white-space: pre-wrap;">
Expand All @@ -16,8 +17,15 @@ <h2>Messages</h2>
<hr class="solid">
{% endif %}

<a href="{% url 'main:boot_process' %}" class="btn btn-primary">Boot</a>
{% render_table table %}
<form method="post", action="{% url 'main:process_action' %}">
{% csrf_token %}
<a href="{% url 'main:boot_process' %}" class="btn btn-primary">Boot</a>
<input type="submit" value="Restart" class="btn btn-success", name="action", onclick="return confirm('Restart selected processes?')">
<input type="submit" value="Flush" class="btn btn-warning", name="action", onclick="return confirm('Flush selected processes?')">
<input type="submit" value="Kill" class="btn btn-danger", name="action", onclick="return confirm('Kill selected processes?')">
{% render_table table %}
</form>

</div>


Expand Down
4 changes: 1 addition & 3 deletions main/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
urlpatterns = [
path("", views.index, name="index"),
path("accounts/", include("django.contrib.auth.urls")),
path("restart/<uuid:uuid>", views.restart_process, name="restart"),
path("kill/<uuid:uuid>", views.kill_process, name="kill"),
path("flush/<uuid:uuid>", views.flush_process, name="flush"),
path("process_action/", views.process_action, name="process_action"),
path("logs/<uuid:uuid>", views.logs, name="logs"),
path("boot_process/", views.BootProcessView.as_view(), name="boot_process"),
path("message/", views.deposit_message, name="message"),
Expand Down
61 changes: 21 additions & 40 deletions main/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,68 +93,49 @@ class ProcessAction(Enum):
FLUSH = "flush"


async def _process_call(uuid: str, action: ProcessAction) -> None:
async def _process_call(uuids: list[str], action: ProcessAction) -> None:
"""Perform an action on a process with a given UUID.

Args:
uuid: UUID of the process to be actioned.
action: Action to be performed {restart,kill}.
uuids: List of UUIDs of the process to be actioned.
action: Action to be performed {restart,flush,kill}.
"""
pmd = get_process_manager_driver()
query = ProcessQuery(uuids=[ProcessUUID(uuid=uuid)])
uuids_ = [ProcessUUID(uuid=u) for u in uuids]

match action:
case ProcessAction.RESTART:
await pmd.restart(query)
for uuid_ in uuids_:
query = ProcessQuery(uuids=[uuid_])
await pmd.restart(query)
Comment on lines +108 to +110
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this different logic here? It makes things rather awkward and I'd be tempted to stick with the previous implementation, particularly if we take out the awaits.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pmd.restart do not work with more than one uuid at the same time, so this one needs a loop. Where we put the loop, I don't mind.

case ProcessAction.KILL:
query = ProcessQuery(uuids=uuids_)
await pmd.kill(query)
case ProcessAction.FLUSH:
query = ProcessQuery(uuids=uuids_)
await pmd.flush(query)


@login_required
def restart_process(request: HttpRequest, uuid: uuid.UUID) -> HttpResponse:
"""Restart the process associated to the given UUID.

Args:
request: HttpRequest object. This is not used in the function, but is required
by Django.
uuid: UUID of the process to be restarted.

Returns:
HttpResponse, redirecting to the main page.
"""
asyncio.run(_process_call(str(uuid), ProcessAction.RESTART))
return HttpResponseRedirect(reverse("main:index"))


@login_required
def kill_process(request: HttpRequest, uuid: uuid.UUID) -> HttpResponse:
"""Kill the process associated to the given UUID.
def process_action(request: HttpRequest) -> HttpResponse:
"""Perform an action on the selected processes.

Args:
request: Django HttpRequest object (unused, but required by Django).
uuid: UUID of the process to be killed.

Returns:
HttpResponse redirecting to the index page.
"""
asyncio.run(_process_call(str(uuid), ProcessAction.KILL))
return HttpResponseRedirect(reverse("main:index"))


@login_required
def flush_process(request: HttpRequest, uuid: uuid.UUID) -> HttpResponse:
"""Flush the process associated to the given UUID.
Both the action and the selected processes are retrieved from the request.

Args:
request: Django HttpRequest object (unused, but required by Django).
uuid: UUID of the process to be flushed.
request: Django HttpRequest object.

Returns:
HttpResponse redirecting to the index page.
"""
asyncio.run(_process_call(str(uuid), ProcessAction.FLUSH))
try:
action = request.POST.get("action", "")
action_enum = ProcessAction(action.lower())
except ValueError:
return HttpResponseRedirect(reverse("main:index"))

if uuids_ := request.POST.getlist("select"):
asyncio.run(_process_call(uuids_, action_enum))
return HttpResponseRedirect(reverse("main:index"))


Expand Down
56 changes: 24 additions & 32 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,6 @@ def test_login_redirect(self, client):
assertRedirects(response, reverse("main:login") + f"?next={self.endpoint}")


class ProcessActionsTest(LoginRequiredTest):
"""Grouping the tests for the process action views."""

action: ProcessAction

@classmethod
def setup_class(cls):
"""Set up the endpoint for the tests."""
cls.uuid = uuid4()
cls.endpoint = reverse(f"main:{cls.action.value}", kwargs=dict(uuid=cls.uuid))

def test_process_action_view_authenticated(self, auth_client, mocker):
"""Test the process action view for an authenticated user."""
mock = mocker.patch("main.views._process_call")
response = auth_client.get(self.endpoint)
assert response.status_code == HTTPStatus.FOUND
assert response.url == reverse("main:index")

mock.assert_called_once_with(str(self.uuid), self.action)


class TestIndexView(LoginRequiredTest):
"""Tests for the index view."""

Expand Down Expand Up @@ -81,22 +60,35 @@ def test_logs_view_authenticated(self, auth_client, mocker):
assert "log_text" in response.context


class TestProcessFlushView(ProcessActionsTest):
"""Tests for the process flush view."""

action = ProcessAction.FLUSH
class TestProcessActionView(LoginRequiredTest):
"""Tests for the process_action view."""

endpoint = reverse("main:process_action")

class TestProcessKillView(ProcessActionsTest):
"""Tests for the process kill view."""

action = ProcessAction.KILL
def test_process_action_no_action(self, auth_client):
"""Test process_action view with no action provided."""
response = auth_client.post(self.endpoint, data={})
assert response.status_code == HTTPStatus.FOUND
assert response.url == reverse("main:index")

def test_process_action_invalid_action(self, auth_client):
"""Test process_action view with an invalid action."""
response = auth_client.post(self.endpoint, data={"action": "invalid_action"})
assert response.status_code == HTTPStatus.FOUND
assert response.url == reverse("main:index")

class TestProcessRestartView(ProcessActionsTest):
"""Tests for the process restart view."""
@pytest.mark.parametrize("action", ["kill", "restart", "flush"])
def test_process_action_valid_action(self, action, auth_client, mocker):
"""Test process_action view with a valid action."""
mock = mocker.patch("main.views._process_call")
uuids_ = [str(uuid4()), str(uuid4())]
response = auth_client.post(
self.endpoint, data={"action": action, "select": uuids_}
)
assert response.status_code == HTTPStatus.FOUND
assert response.url == reverse("main:index")

action = ProcessAction.RESTART
mock.assert_called_once_with(uuids_, ProcessAction(action))


class TestBootProcess(LoginRequiredTest):
Expand Down