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

Initialise the controller app #132

Merged
merged 30 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
18d3695
Initial django startapp controller
AdrianDAlessandro Oct 3, 2024
9c2eba2
Move controller tests into tests directory
AdrianDAlessandro Oct 3, 2024
f7e3047
Make pre-commit happy
AdrianDAlessandro Oct 3, 2024
57a913c
Add controller to installed apps
AdrianDAlessandro Oct 3, 2024
e52c44f
Create index view for controller
AdrianDAlessandro Oct 3, 2024
6e5ad26
Add controller link to navbar
AdrianDAlessandro Oct 3, 2024
93cbdc6
Add test for controller view
AdrianDAlessandro Oct 3, 2024
13c8507
Merge branch 'main' into controller-app
AdrianDAlessandro Oct 4, 2024
6749f6f
Initial django startapp process_manager
AdrianDAlessandro Oct 4, 2024
34f966f
Make pre-commit happy
AdrianDAlessandro Oct 4, 2024
3134224
Add process_manager to installed apps
AdrianDAlessandro Oct 4, 2024
f103a67
Copy views, forms, tables and urls from main to process_manager
AdrianDAlessandro Oct 4, 2024
eea50e9
Remove process_manager views from main/views.py
AdrianDAlessandro Oct 4, 2024
7b5cf1b
Namespace process_manager urls and views
AdrianDAlessandro Oct 4, 2024
b7b947f
Move process_manager templates from main
AdrianDAlessandro Oct 4, 2024
901be01
Require login for main index
AdrianDAlessandro Oct 4, 2024
6539087
Add buttons to main index template
AdrianDAlessandro Oct 4, 2024
97ef845
Move navbar to separate template file
AdrianDAlessandro Oct 4, 2024
67a5ead
Add process manager to navbar and require authentication to see it
AdrianDAlessandro Oct 4, 2024
de8b1c1
Make pre-commit happy
AdrianDAlessandro Oct 4, 2024
186a807
Move process_manager tests
AdrianDAlessandro Oct 4, 2024
bf521fd
Create tests/main
AdrianDAlessandro Oct 4, 2024
9ee5c63
Remove process_manager views tests from main views tests
AdrianDAlessandro Oct 4, 2024
bfd4ff8
Fix all paths and url namespaces in tests
AdrianDAlessandro Oct 4, 2024
14d7ded
Assert the main index view contains buttons and nav links
AdrianDAlessandro Oct 4, 2024
807c439
Fix docstring
AdrianDAlessandro Oct 4, 2024
4a26086
Merge branch 'main' into controller-app
AdrianDAlessandro Oct 4, 2024
b78d0f6
Merge branch 'controller-app' into process-manager-app
AdrianDAlessandro Oct 4, 2024
1db2be1
Merge pull request #133 from ImperialCollegeLondon/process-manager-app
cc-a Oct 7, 2024
ba75741
Merge branch 'main' into controller-app
cc-a Oct 7, 2024
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
1 change: 1 addition & 0 deletions controller/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""The controller app for the drunc_ui project."""
3 changes: 3 additions & 0 deletions controller/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Admin module for the controller app."""

# Register your models here.
10 changes: 10 additions & 0 deletions controller/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Apps module for the controller app."""

from django.apps import AppConfig


class ControllerConfig(AppConfig):
"""The app config for the controller app."""

default_auto_field = "django.db.models.BigAutoField"
name = "controller"
Empty file.
3 changes: 3 additions & 0 deletions controller/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Models module for the controller app."""

# Create your models here.
4 changes: 4 additions & 0 deletions controller/templates/controller/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% extends "main/base.html" %}
{% block title %}
Controller
{% endblock title %}
10 changes: 10 additions & 0 deletions controller/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Urls module for the controller app."""

from django.urls import path

from . import views

app_name = "controller"
urlpatterns = [
path("", views.index, name="index"),
]
11 changes: 11 additions & 0 deletions controller/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Views module for the controller app."""

from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render


@login_required
def index(request: HttpRequest) -> HttpResponse:
"""View that renders the index/home page."""
return render(request=request, template_name="controller/index.html")
2 changes: 1 addition & 1 deletion drunc_ui/settings/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@


# Custom settings
INSTALLED_APPS += ["main", "django_tables2"]
INSTALLED_APPS += ["main", "process_manager", "controller", "django_tables2"]


MIDDLEWARE.insert(1, "whitenoise.middleware.WhiteNoiseMiddleware")
Expand Down
2 changes: 2 additions & 0 deletions drunc_ui/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("main.urls")),
path("process_manager/", include("process_manager.urls")),
path("controller/", include("controller.urls")),
]
6 changes: 1 addition & 5 deletions main/templates/main/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,7 @@
<script src="{% static 'main/js/htmx.min.js' %}"></script>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="{% url 'main:index' %}">DUNE Process Manager</a>
</div>
</nav>
{% include "main/navbar.html" %}
<div class="container py-3">
{% block content %}
{% endblock content %}
Expand Down
44 changes: 2 additions & 42 deletions main/templates/main/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,11 @@
{% block title %}
Home
{% endblock title %}
{% block extra_css %}
<style>
.card-body {
max-height: 85vh;
overflow-y: auto;
}
</style>
{% endblock extra_css %}
{% block content %}
<div class="row">
<div class="col">
<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?')">
<div hx-get="{% url 'main:process_table' %}"
hx-swap="outerHTML"
hx-trigger="load"></div>
</form>
</div>
<div class="col">
<div class="card">
<div class="card-header">Messages</div>
<div class="card-body">
<ul class="list-group">
{% if messages %}
{% for message in messages %}<li class="list-group-item">{{ message }}</li>{% endfor %}
{% endif %}
</ul>
</div>
</div>
<a href="{% url 'process_manager:index' %}" class="btn btn-secondary">Process Manager</a>
<a href="{% url 'controller:index' %}" class="btn btn-secondary">Controller</a>
</div>
</div>
{% endblock content %}
27 changes: 27 additions & 0 deletions main/templates/main/navbar.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!--Navbar-->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="{% url 'main:index' %}">DUNE Run Control</a>
<button class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarLinks">
<ul class="navbar-nav me-auto">
{% if user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{% url 'process_manager:index' %}">Process Manager</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'controller:index' %}">Controller</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
9 changes: 0 additions & 9 deletions main/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,7 @@
from . import views

app_name = "main"

partial_urlpatterns = [
path("process_table/", views.process_table, name="process_table"),
]

urlpatterns = [
path("", views.index, name="index"),
path("accounts/", include("django.contrib.auth.urls")),
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("partials/", include(partial_urlpatterns)),
]
213 changes: 3 additions & 210 deletions main/views.py
Original file line number Diff line number Diff line change
@@ -1,218 +1,11 @@
"""Views for the main app."""

import asyncio
import uuid
from enum import Enum

import django_tables2
from django.conf import settings
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db import transaction
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from django.urls import reverse, reverse_lazy
from django.views.generic.edit import FormView
from drunc.process_manager.process_manager_driver import ProcessManagerDriver
from drunc.utils.shell_utils import DecodedResponse, create_dummy_token_from_uname
from druncschema.process_manager_pb2 import (
LogRequest,
ProcessInstance,
ProcessInstanceList,
ProcessQuery,
ProcessUUID,
)

from .forms import BootProcessForm
from .tables import ProcessTable


def get_process_manager_driver() -> ProcessManagerDriver:
"""Get a ProcessManagerDriver instance."""
token = create_dummy_token_from_uname()
return ProcessManagerDriver(
settings.PROCESS_MANAGER_URL, token=token, aio_channel=True
)


async def get_session_info() -> ProcessInstanceList:
"""Get info about all sessions from process manager."""
pmd = get_process_manager_driver()
query = ProcessQuery(names=[".*"])
return await pmd.ps(query)


@login_required
def index(request: HttpRequest) -> HttpResponse:
"""View that renders the index/home page."""
with transaction.atomic():
# atomic to avoid race condition with kafka consumer
messages = request.session.load().get("messages", [])
request.session.pop("messages", [])
request.session.save()

context = {"messages": messages}
return render(request=request, context=context, template_name="main/index.html")


@login_required
def process_table(request: HttpRequest) -> HttpResponse:
"""Renders the process table.

This view may be called using either GET or POST methods. GET renders the table with
no check boxes selected. POST renders the table with checked boxes for any table row
with a uuid provided in the select key of the request data.
"""
selected_rows = request.POST.getlist("select", [])
session_info = asyncio.run(get_session_info())

status_enum_lookup = dict(item[::-1] for item in ProcessInstance.StatusCode.items())

table_data = []
process_instances = session_info.data.values
for process_instance in process_instances:
metadata = process_instance.process_description.metadata
uuid = process_instance.uuid.uuid
table_data.append(
{
"uuid": uuid,
"name": metadata.name,
"user": metadata.user,
"session": metadata.session,
"status_code": status_enum_lookup[process_instance.status_code],
"exit_code": process_instance.return_code,
"checked": (uuid in selected_rows),
}
)
table = ProcessTable(table_data)

# sort table data based on request parameters
table_configurator = django_tables2.RequestConfig(request)
table_configurator.configure(table)

return render(
request=request,
context=dict(table=table),
template_name="main/partials/process_table.html",
)


# an enum for process actions
class ProcessAction(Enum):
"""Enum for process actions."""

RESTART = "restart"
KILL = "kill"
FLUSH = "flush"


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

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

match action:
case ProcessAction.RESTART:
for uuid_ in uuids_:
query = ProcessQuery(uuids=[uuid_])
await pmd.restart(query)
case ProcessAction.KILL:
query = ProcessQuery(uuids=uuids_)
await pmd.kill(query)
case ProcessAction.FLUSH:
query = ProcessQuery(uuids=uuids_)
await pmd.flush(query)


@login_required
@permission_required("main.can_modify_processes", raise_exception=True)
def process_action(request: HttpRequest) -> HttpResponse:
"""Perform an action on the selected processes.

Both the action and the selected processes are retrieved from the request.

Args:
request: Django HttpRequest object.

Returns:
HttpResponse redirecting to the index page.
"""
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"))


async def _get_process_logs(uuid: str) -> list[DecodedResponse]:
"""Retrieve logs for a process from the process manager.

Args:
uuid: UUID of the process.

Returns:
The process logs.
"""
pmd = get_process_manager_driver()
query = ProcessQuery(uuids=[ProcessUUID(uuid=uuid)])
request = LogRequest(query=query, how_far=100)
return [item async for item in pmd.logs(request)]


@login_required
@permission_required("main.can_view_process_logs", raise_exception=True)
def logs(request: HttpRequest, uuid: uuid.UUID) -> HttpResponse:
"""Display the logs of a process.

Args:
request: the triggering request.
uuid: identifier for the process.

Returns:
The rendered page.
"""
logs_response = asyncio.run(_get_process_logs(str(uuid)))
context = dict(log_text="\n".join(val.data.line for val in logs_response))
return render(request=request, context=context, template_name="main/logs.html")


async def _boot_process(user: str, data: dict[str, str | int]) -> None:
"""Boot a process with the given data.

Args:
user: the user to boot the process as.
data: the data for the process.
"""
pmd = get_process_manager_driver()
async for item in pmd.dummy_boot(user=user, **data):
pass


class BootProcessView(PermissionRequiredMixin, FormView): # type: ignore [type-arg]
"""View for the BootProcess form."""

template_name = "main/boot_process.html"
form_class = BootProcessForm
success_url = reverse_lazy("main:index")
permission_required = "main.can_modify_processes"

def form_valid(self, form: BootProcessForm) -> HttpResponse:
"""Boot a Process when valid form data has been POSTed.

Args:
form: the form instance that has been validated.

Returns:
A redirect to the index page.
"""
asyncio.run(_boot_process("root", form.cleaned_data))
return super().form_valid(form)
return render(request=request, template_name="main/index.html")
Loading