Skip to content

Commit

Permalink
feat: added a new quickview handler
Browse files Browse the repository at this point in the history
  • Loading branch information
orenlab committed Jan 13, 2025
1 parent eb2db25 commit 8bc5532
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 42 deletions.
2 changes: 1 addition & 1 deletion pytmbot/handlers/bot_handlers/inline/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def handle_update_info(call: CallbackQuery, bot: TeleBot):
chat_id=call.message.chat.id,
message_id=call.message.message_id,
text=bot_answer,
parse_mode="Markdown",
parse_mode="HTML",
)
except Exception as error:
bot.edit_message_text(
Expand Down
7 changes: 7 additions & 0 deletions pytmbot/handlers/handler_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from pytmbot.handlers.server_handlers.memory import handle_memory
from pytmbot.handlers.server_handlers.network import handle_network
from pytmbot.handlers.server_handlers.process import handle_process
from pytmbot.handlers.server_handlers.quickview import handle_quick_view
from pytmbot.handlers.server_handlers.sensors import handle_sensors
from pytmbot.handlers.server_handlers.server import handle_server
from pytmbot.handlers.server_handlers.uptime import handle_uptime
Expand Down Expand Up @@ -92,6 +93,12 @@ def handler_factory() -> HandlerType:
regexp="Enter 2FA code"
)
],
"quick_view": [
HandlerConfig(
callback=handle_quick_view,
regexp="Quick view"
)
],
"code_verification": [
HandlerConfig(
callback=handle_totp_code_verification,
Expand Down
182 changes: 182 additions & 0 deletions pytmbot/handlers/server_handlers/quickview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
#!/venv/bin/python3
"""
(c) Copyright 2024, Denis Rozhnovskiy <[email protected]>
pyTMBot - A simple Telegram bot to handle Docker containers and images,
also providing basic information about the status of local servers.
"""
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Dict, Any, Optional

from telebot import TeleBot
from telebot.types import Message

from pytmbot import exceptions
from pytmbot.adapters.docker.containers_info import fetch_docker_counters
from pytmbot.exceptions import ErrorContext
from pytmbot.globals import psutil_adapter, em
from pytmbot.logs import Logger
from pytmbot.parsers.compiler import Compiler

logger = Logger()


def _get_uptime() -> Optional[str]:
"""Get system uptime."""
try:
return psutil_adapter.get_uptime()
except Exception as e:
logger.error(f"Failed to get uptime: {e}")
return None


def _get_load() -> Optional[tuple]:
"""Get load average."""
try:
return psutil_adapter.get_load_average()
except Exception as e:
logger.error(f"Failed to get load average: {e}")
return None


def _get_memory() -> Optional[Dict]:
"""Get memory statistics."""
try:
return psutil_adapter.get_memory()
except Exception as e:
logger.error(f"Failed to get memory stats: {e}")
return None


def _get_processes() -> Optional[Dict]:
"""Get process counts."""
try:
return psutil_adapter.get_process_counts()
except Exception as e:
logger.error(f"Failed to get process counts: {e}")
return None


def _get_cpu() -> Optional[Dict]:
"""Get CPU usage."""
try:
return psutil_adapter.get_cpu_usage()
except Exception as e:
logger.error(f"Failed to get CPU usage: {e}")
return None


def _get_docker() -> Optional[Dict]:
"""Get Docker statistics."""
try:
return fetch_docker_counters()
except Exception as e:
logger.error(f"Failed to get Docker stats: {e}")
return None


def _collect_metrics() -> Dict[str, Any]:
"""
Collect all metrics concurrently using ThreadPoolExecutor.
Returns:
Dict containing all collected metrics.
"""
metrics = {}

# Define tasks to run concurrently
tasks = {
'uptime': _get_uptime,
'load_average': _get_load,
'memory': _get_memory,
'processes': _get_processes,
'cpu': _get_cpu,
'docker': _get_docker
}

with ThreadPoolExecutor(max_workers=len(tasks)) as executor:
# Start all tasks
future_to_task = {
executor.submit(func): name
for name, func in tasks.items()
}

# Collect results as they complete
for future in as_completed(future_to_task):
task_name = future_to_task[future]
try:
result = future.result()
if result is not None:
metrics[task_name] = result
else:
logger.warning(f"Task {task_name} returned None")
except Exception as e:
logger.error(f"Task {task_name} generated an exception: {e}")

return metrics


# regexp="Quick view|Quick status|qv"
@logger.session_decorator
def handle_quick_view(message: Message, bot: TeleBot):
"""Handle quick view command to show system and Docker summary."""
emojis = {
"computer": em.get_emoji("desktop_computer"),
"chart": em.get_emoji("bar_chart"),
"memory": em.get_emoji("brain"),
"cpu": em.get_emoji("electric_plug"),
"process": em.get_emoji("gear"),
"docker": em.get_emoji("whale"),
"warning": em.get_emoji("warning"),
}

try:
bot.send_chat_action(message.chat.id, "typing")

# Collect all metrics concurrently
metrics = _collect_metrics()

if not metrics:
logger.error("Failed to collect any metrics for quick view")
return bot.send_message(
message.chat.id,
text="⚠️ Failed to get system metrics. Please try again later."
)

# Prepare context for template
context = {
"system": {
"uptime": metrics.get('uptime', 'N/A'),
"load_average": metrics.get('load_average', (0, 0, 0)),
"memory": metrics.get('memory', {}),
"processes": metrics.get('processes', {}),
"cpu": metrics.get('cpu', {})
}
}

# Add Docker metrics if available
if 'docker' in metrics:
context['docker'] = metrics['docker']

with Compiler(
template_name="b_quick_view.jinja2",
context=context,
**emojis
) as compiler:
bot_answer = compiler.compile()

bot.send_message(
message.chat.id,
text=bot_answer,
parse_mode="Markdown"
)

except Exception as error:
bot.send_message(
message.chat.id,
"⚠️ An error occurred while processing the command."
)
raise exceptions.HandlingException(ErrorContext(
message="Failed handling quick view command",
error_code="HAND_QV1",
metadata={"exception": str(error)}
))
1 change: 1 addition & 0 deletions pytmbot/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def get_default_main_keyboard() -> Dict[str, str]:
"rocket": "Server",
"spouting_whale": "Docker",
"lollipop": "Plugins",
"eyes": "Quick view",
"mushroom": "About me"

}
Expand Down
73 changes: 32 additions & 41 deletions pytmbot/templates/base_templates/b_how_update.jinja2
Original file line number Diff line number Diff line change
@@ -1,41 +1,32 @@
{{ thought_balloon }} Here is:

🏗 **Updating the image**

To update the image to the latest version, please follow these steps:

1. **Stop the running pyTMbot container:**
```bash
sudo docker stop pytmbot
```

2. **Delete the outdated container:**
```bash
sudo docker rm pytmbot
```

3. **Delete the outdated image:**
```bash
sudo docker rmi pytmbot
```

4. **Pull the updated image:**
```bash
sudo docker pull orenlab/pytmbot:latest
```

5. **Run the container as you would if you had just installed the bot:**

```bash
sudo docker run -d \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /root/pytmbot.yaml:/opt/pytmbot/pytmbot.yaml:ro \
--env TZ="Asia/Yekaterinburg" \
--restart=always \
--name=pytmbot \
--pid=host \
--security-opt=no-new-privileges \
orenlab/pytmbot:latest
```

*Note: Please don't forget to adjust the time zone!*
{{ thought_balloon }} <b>Updating PyTMBot to Latest Version</b>

Choose your preferred update method:

<b>A. Using Docker Compose (Recommended)</b>

1) Update your docker-compose.yml:

<code>docker compose pull && docker compose up -d</code>

<b>B. Using Docker CLI</b>

1) <code>docker stop pytmbot && docker rm pytmbot</code>

2) <code>docker pull orenlab/pytmbot:latest</code>

3) Run new container:
<pre>docker run -d \
--name pytmbot \
--restart on-failure \
--env TZ="Asia/Yekaterinburg" \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /root/pytmbot.yaml:/opt/app/pytmbot.yaml:ro \
--security-opt no-new-privileges \
--read-only \
--cap-drop ALL \
--pid=host \
--log-opt max-size=10m \
--log-opt max-file=3 \
orenlab/pytmbot:latest --plugins SOME_PLUGINS</pre>

⚠️ <b>Note</b>: <i>Adjust timezone as needed!</i>
30 changes: 30 additions & 0 deletions pytmbot/templates/base_templates/b_quick_view.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{# templates/b_quick_view.jinja2 #}
🖥️ *System Quick View*

```bash
{% if context.system %}
🖥️
├─ ⏱️ Uptime: {{ "{:>10}".format(context.system.uptime if context.system.uptime else 'N/A') }}
├─ 📊 Load Average:
│ ├─ 1 min: {{ "{:>10.2f}".format(context.system.load_average[0] if context.system.load_average and context.system.load_average[0] is defined else 'N/A') }}
│ ├─ 5 min: {{ "{:>10.2f}".format(context.system.load_average[1] if context.system.load_average and context.system.load_average[1] is defined else 'N/A') }}
│ └─ 15 min: {{ "{:>10.2f}".format(context.system.load_average[2] if context.system.load_average and context.system.load_average[2] is defined else 'N/A') }}
├─ 🧠 Memory:
│ ├─ Used: {{ "{:>10}".format(context.system.memory.used if context.system.memory and context.system.memory.used else 'N/A') }}
│ ├─ Free: {{ "{:>10}".format(context.system.memory.free if context.system.memory and context.system.memory.free else 'N/A') }}
│ └─ Usage: {{ "{:>10}".format((context.system.memory.percent | string + '%') if context.system.memory and context.system.memory.percent is not none else 'N/A') }}
├─ ⚡ CPU Usage:
│ └─ {{ "{:>10}".format((context.system.cpu.cpu_percent | string + '%') if context.system.cpu and context.system.cpu.cpu_percent is not none else 'N/A') }}
└─ ⚙️ Processes:
├─ Running: {{ "{:>10}".format(context.system.processes.running if context.system.processes and context.system.processes.running else 'N/A') }}
└─ Total: {{ "{:>10}".format(context.system.processes.total if context.system.processes and context.system.processes.total else 'N/A') }}
{% else %}
⚠️ System data is not available.
{% endif %}
{% if context.docker %}
🐳
└─ 🐳 Docker Status
├─ Containers: {{ "{:>10}".format(context.docker.containers_count if context.docker.containers_count else 'N/A') }}
└─ Images: {{ "{:>10}".format(context.docker.images_count if context.docker.images_count else 'N/A') }}
{% endif %}
```
Empty file removed pytmbot/utils/utilities.py
Empty file.

0 comments on commit 8bc5532

Please sign in to comment.