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

fixing stale process handling #14

Merged
merged 4 commits into from
Dec 31, 2023
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
2 changes: 2 additions & 0 deletions docs/source/version_history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ Version History
"0.0.5", "removing ajax view; using only htmx"
"0.0.6", "updating templates for latest Bootstrap"
"0.0.7", "adding isort; adding badges to readme"
"0.0.8", "fixed modal trigger on host process card"
"0.0.9", "fixed stale process handling in host process view; updated modals"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ ignore_missing_imports = true

# https://docs.pytest.org/en/6.2.x/customize.html#pyproject-toml
[tool.pytest.ini_options]
addopts = "-s -v -x --strict-markers -m 'not extra' --doctest-modules"
#addopts = "-s -v -x --strict-markers -m 'not extra' --doctest-modules"
filterwarnings = [
"ignore::UserWarning",
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@
<ul class="list-group mx-auto justify-content-center" style="max-width: 93%; margin-top: .5%; margin-bottom: .5%">
{% for cpu, data in cpu_data.items %}
<li class="list-group-item bg-light shadow-sm hvr-grow mb-3">
<a href="#" hx-get="{% url 'hostutils:get_cpu_stats' cpu %}" hx-target="#modal_wrapper">
<a href="#" hx-get="{% url 'hostutils:get_cpu_stats' cpu %}" hx-target="#modal_wrapper" data-bs-toggle="modal" data-bs-target="#modal_wrapper">
<div class="row">
<div class="col-xs-12 col-md-2"><span class="key">CPU:</span><span class="value">{{ cpu }}</span></div>
<div class="col-xs-12 col-md-2"><span class="key">Percent:</span><span class="value">{{ data.percent }}%</span></div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@
<ul class="list-group mx-auto justify-content-center" style="max-width: 93%; margin-top: .5%; margin-bottom: .5%">
{% for item in partition_lists %}
<li class="list-group-item bg-light shadow-sm hvr-grow mb-3">
<a href="#" hx-get="{% url 'hostutils:get_partition_stats' %}?part={{ item.mountpoint }}" hx-target="#modal_wrapper">
<a href="#" hx-get="{% url 'hostutils:get_partition_stats' %}?part={{ item.mountpoint }}" hx-target="#modal_wrapper" data-bs-toggle="modal" data-bs-target="#modal_wrapper">
<div class="row mb-1">
<div class="col-xs-12 col-md-3"><span class="key">Device:</span><span class="value">{{ item.device }}</span></div>
<div class="col-xs-12 col-md-2"><span class="key">Filesystem:</span><span class="value">{{ item.fstype }}</span></div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@
<ul class="list-group mx-auto justify-content-center" style="max-width: 93%; margin-top: .5%; margin-bottom: .5%">
{% for k,v in interface_list.items %}
<li class="list-group-item bg-light shadow-sm hvr-grow mb-3">
<a href="#" hx-get="{% url 'hostutils:get_interface_stats' k %}" hx-target="#modal_wrapper">
<a href="#" hx-get="{% url 'hostutils:get_interface_stats' k %}" hx-target="#modal_wrapper" data-bs-toggle="modal" data-bs-target="#modal_wrapper">
<div class="row">
<div class="key h5">{{ k }}:</div>
{% for item in v %}
Expand Down
38 changes: 16 additions & 22 deletions src/djangoaddicts/hostutils/views/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from django.views.generic import View

# import forms
from djangoaddicts.hostutils.forms import HostProcessFilterForm


class ShowHost(View):
Expand Down Expand Up @@ -139,26 +138,21 @@ def get(self, request, *args, **kwargs):
context["title"] = self.title
context["now"] = datetime.datetime.now()
context["subtitle"] = psutil.os.uname()[1]
process_list = list(psutil.process_iter())
context["process_list"] = process_list
counts = {
"running": len([i for i in process_list if i.status() == "running"]),
"sleeping": len([i for i in process_list if i.status() == "sleeping"]),
"idle": len([i for i in process_list if i.status() == "idle"]),
"stopped": len([i for i in process_list if i.status() == "stopped"]),
"zombie": len([i for i in process_list if i.status() == "zombie"]),
"dead": len([i for i in process_list if i.status() == "dead"]),
}
counts = {"running": 0, "sleeping": 0, "idle": 0, "stopped": 0, "zombie": 0, "dead": 0, "disk-sleep": 0}
process_list = []
for process in psutil.process_iter():
try:
counts[process.status()] += 1
process_list.append(
{
"pid": process.pid,
"name": process.name(),
"status": process.status(),
"started_at": process.create_time(),
}
)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
context["counts"] = counts
filter_form = {}
filter_form["form"] = HostProcessFilterForm(request.GET or None)
filter_form["modal_name"] = "filter_processes"
filter_form["modal_size"] = "modal-lg"
filter_form["modal_title"] = "Filter Host Processes"
filter_form["hx_method"] = "hx-get"
filter_form["hx_url"] = "/hostutils/get_host_processes"
filter_form["hx_target"] = "id_process_list_container"
filter_form["method"] = "GET"
filter_form["action"] = "Filter"
context["filter_form"] = filter_form
context["process_list"] = process_list
return render(request, self.template_name, context=context)
61 changes: 23 additions & 38 deletions src/djangoaddicts/hostutils/views/htmx.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
class GetHostCpuStats(BuildBootstrapModalView):
"""Get statistics for a given CPU"""

modal_button_close = None
modal_button_submit = "Close"
modal_button_submit = None
modal_size = "modal-lg"
modal_title = "CPU Details"

Expand All @@ -38,8 +37,7 @@ def get(self, request, *args, **kwargs):
class GetHostNetworkStats(BuildBootstrapModalView):
"""Get statistics for a given network interface"""

modal_button_close = None
modal_button_submit = "Close"
modal_button_submit = None
modal_size = "modal-lg"
modal_title = "Network Interface Details"

Expand All @@ -57,8 +55,7 @@ def get(self, request, *args, **kwargs):
class GetHostParitionStats(BuildBootstrapModalView):
"""Get statistics for a given disk partition"""

modal_button_close = None
modal_button_submit = "Close"
modal_button_submit = None
modal_title = "Partition Details"

def get(self, request, *args, **kwargs):
Expand All @@ -76,39 +73,27 @@ def get(self, request, *args, **kwargs):
class GetHostProcesses(View):
"""Get host processes"""

@staticmethod
def get_process_count(process_list: list, status: str) -> int:
"""get a count of processes for a given status

Args:
process_list (list): list of processes as returned from psutil.process_iter()
status (str): name of process status to count

Returns:
int: number of processes of 'status'
"""
count = 0
for process in process_list:
try:
if process.status() == status:
count += 1
except psutil.NoSuchProcess:
continue
return count

def get(self, request):
"""Get host prcesses"""
context = {}
process_list = list(psutil.process_iter())
filter_form = HostProcessFilterForm(request.GET or None)
context["counts"] = {
"running": self.get_process_count(process_list, "running"),
"sleeping": self.get_process_count(process_list, "sleeping"),
"idle": self.get_process_count(process_list, "idle"),
"stopped": self.get_process_count(process_list, "stopped"),
"zombie": self.get_process_count(process_list, "zombie"),
"dead": self.get_process_count(process_list, "dead"),
}
counts = {"running": 0, "sleeping": 0, "idle": 0, "stopped": 0, "zombie": 0, "dead": 0, "disk-sleep": 0}

process_list = []
for process in psutil.process_iter():
try:
counts[process.status()] += 1
process_list.append(
{
"pid": process.pid,
"name": process.name(),
"status": process.status(),
"started_at": process.create_time(),
}
)
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
context["counts"] = counts

if request.GET.dict().get("clear", None):
context["clear_filter"] = False
Expand All @@ -120,20 +105,20 @@ def get(self, request):
if filter_form.cleaned_data.get("status", None):
filtered_process_list = []
for i in process_list:
if i.status() in filter_form.cleaned_data["status"]:
if i["status"] in filter_form.cleaned_data["status"]:
filtered_process_list.append(i)
process_list = filtered_process_list

if filter_form.cleaned_data.get("created_at__gte", None):
filtered_process_list = []
for i in process_list:
if i.create_time() > filter_form.cleaned_data["created_at__gte"].timestamp():
if i["started_at"] > filter_form.cleaned_data["created_at__gte"].timestamp():
filtered_process_list.append(i)
process_list = filtered_process_list
if filter_form.cleaned_data.get("created_at__lte", None):
filtered_process_list = []
for i in process_list:
if i.create_time() < filter_form.cleaned_data["created_at__lte"].timestamp():
if i["started_at"] < filter_form.cleaned_data["created_at__lte"].timestamp():
filtered_process_list.append(i)
process_list = filtered_process_list

Expand Down
17 changes: 17 additions & 0 deletions tests/unit/test_gui.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from django.test import TestCase
from django.shortcuts import reverse
from unittest.mock import patch
import psutil
import subprocess
import itertools


class ShowHostCpuTests(TestCase):
Expand Down Expand Up @@ -66,3 +70,16 @@ def test_get(self):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "hostutils/bs5/detail/processes.html")

def test_get_with_invalid(self):
"""verify page is redered if psutil.NoSuchProcess is raised"""
url = reverse("hostutils:host_process")
process_list = psutil.process_iter()
with patch("psutil.process_iter") as mocked_process_list:
p = subprocess.Popen("ls", stdout=subprocess.PIPE)
mp = iter((psutil.Process(p.pid), psutil.Process(p.pid)))
p.communicate()
mocked_process_list.return_value = itertools.chain(mp, process_list)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "hostutils/bs5/detail/processes.html")
88 changes: 40 additions & 48 deletions tests/unit/test_htmx.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

class GetHostProcessessTests(TestCase):
"""test GetHostProcesses ajax view"""

def setUp(self):
super(GetHostProcessessTests, self).setUp()
self.url = reverse("hostutils:get_host_processes")
Expand Down Expand Up @@ -45,70 +46,59 @@ def test_get_with_status_running(self):
self.client.get(self.url, data={"status": "running"}, **self.headers)

def test_get_with_invalid(self):
with self.assertRaises(psutil.NoSuchProcess):
process_list = psutil.process_iter()
with patch("psutil.process_iter") as mocked_process_list:
p = subprocess.Popen("ls", stdout=subprocess.PIPE)
mp = iter((psutil.Process(p.pid), psutil.Process(p.pid)))
p.communicate()
mocked_process_list.return_value = itertools.chain(mp, process_list)
self.client.get(self.url, **self.headers)

def test_get_with_status_invalid(self):
with patch("psutil.Process.status") as status:
status.return_value = "running"
process_list = psutil.process_iter()
with patch('psutil.process_iter') as mocked_process_list:
p = subprocess.Popen('ls', stdout=subprocess.PIPE)
with patch("psutil.process_iter") as mocked_process_list:
p = subprocess.Popen("ls", stdout=subprocess.PIPE)
mp = iter((psutil.Process(p.pid), psutil.Process(p.pid)))
p.communicate()
mocked_process_list.return_value = itertools.chain(
mp,
process_list
)
self.client.get(self.url, **self.headers)

def test_get_with_status_invalid(self):
with self.assertRaises(psutil.NoSuchProcess):
with patch("psutil.Process.status") as status:
status.return_value = "running"
process_list = psutil.process_iter()
with patch('psutil.process_iter') as mocked_process_list:
p = subprocess.Popen('ls', stdout=subprocess.PIPE)
mp = iter((psutil.Process(p.pid), psutil.Process(p.pid)))
p.communicate()
mocked_process_list.return_value = itertools.chain(
mp,
process_list
)
self.client.get(self.url, data={"status": "running"}, **self.headers)
mocked_process_list.return_value = itertools.chain(mp, process_list)
self.client.get(self.url, data={"status": "running"}, **self.headers)

def test_get_with_gte_invalid(self):
with self.assertRaises(psutil.NoSuchProcess):
with patch("psutil.Process.status") as status:
status.return_value = "running"
process_list = psutil.process_iter()
with patch('psutil.process_iter') as mocked_process_list:
p = subprocess.Popen('ls', stdout=subprocess.PIPE)
mp = iter((psutil.Process(p.pid), psutil.Process(p.pid)))
p.communicate()
mocked_process_list.return_value = itertools.chain(
mp,
process_list
)
self.client.get(self.url, data={"status": "running",
"created_at__gte": self.now - timezone.timedelta(days=1)},
**self.headers)
with patch("psutil.Process.status") as status:
status.return_value = "running"
process_list = psutil.process_iter()
with patch("psutil.process_iter") as mocked_process_list:
p = subprocess.Popen("ls", stdout=subprocess.PIPE)
mp = iter((psutil.Process(p.pid), psutil.Process(p.pid)))
p.communicate()
mocked_process_list.return_value = itertools.chain(mp, process_list)
self.client.get(
self.url,
data={"status": "running", "created_at__gte": self.now - timezone.timedelta(days=1)},
**self.headers,
)

def test_get_with_lte_invalid(self):
# with self.assertRaises(psutil.NoSuchProcess):
with patch("psutil.Process.status") as status:
status.return_value = "running"
process_list = psutil.process_iter()
with patch('psutil.process_iter') as mocked_process_list:
p = subprocess.Popen('ls', stdout=subprocess.PIPE)
with patch("psutil.process_iter") as mocked_process_list:
p = subprocess.Popen("ls", stdout=subprocess.PIPE)
mp = iter((psutil.Process(p.pid), psutil.Process(p.pid)))
p.communicate()
mocked_process_list.return_value = itertools.chain(
mp,
process_list
mocked_process_list.return_value = itertools.chain(mp, process_list)
self.client.get(
self.url,
data={"status": "running", "created_at__lte": self.now - timezone.timedelta(days=1)},
**self.headers,
)
self.client.get(self.url, data={"status": "running",
"created_at__lte": self.now - timezone.timedelta(days=1)},
**self.headers)


class GetHostCpuStatsTests(TestCase):
"""test GetHostCpuStats htmx view"""

def setUp(self):
super(GetHostCpuStatsTests, self).setUp()
self.url = reverse("hostutils:get_cpu_stats", kwargs={"cpu": 1})
Expand All @@ -129,9 +119,9 @@ def test_get_with_invalid_request(self):
self.assertEqual(response.status_code, 400)



class GetHostNetworkStatsTests(TestCase):
"""test GetHostNetworkStats htmx view"""

def setUp(self):
super(GetHostNetworkStatsTests, self).setUp()
self.url = reverse("hostutils:get_interface_stats", kwargs={"interface": "lo"})
Expand All @@ -154,6 +144,7 @@ def test_get_with_invalid_request(self):

class GetHostParitionStatsTests(TestCase):
"""test GetHostParitionStats htmx view"""

def setUp(self):
super(GetHostParitionStatsTests, self).setUp()
self.url = reverse("hostutils:get_partition_stats") + "?part=/"
Expand All @@ -176,6 +167,7 @@ def test_get_with_invalid_request(self):

class GetHostProcessStatsTests(TestCase):
"""test GetHostProcessStats htmx view"""

def setUp(self):
super(GetHostProcessStatsTests, self).setUp()
self.url = reverse("hostutils:get_process_stats", kwargs={"pid": 1})
Expand Down
Loading