first commit
This commit is contained in:
Binary file not shown.
+23
@@ -0,0 +1,23 @@
|
|||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: build-and-push
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build-image
|
||||||
|
image: plugins/docker
|
||||||
|
settings:
|
||||||
|
registry: git.sedletskiy.ru
|
||||||
|
repo: git.sedletskiy.ru/kosechok/testmintools
|
||||||
|
tags:
|
||||||
|
- latest
|
||||||
|
- ${DRONE_COMMIT_SHA:0:8}
|
||||||
|
username:
|
||||||
|
from_secret: REGISTRY_USER
|
||||||
|
password:
|
||||||
|
from_secret: REGISTRY_PASSWORD
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
branch:
|
||||||
|
- master
|
||||||
|
event:
|
||||||
|
- push
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
RUN python -m pip install --upgrade pip
|
||||||
|
RUN pip install Flask
|
||||||
|
|
||||||
|
# Установка curl, nano, ping и traceroute
|
||||||
|
RUN apt-get update && apt-get install -y curl nano iputils-ping traceroute
|
||||||
|
RUN mkdir /app
|
||||||
|
COPY ./network_tools.py /app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
CMD ["python", "network_tools.py"]
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
# 🛠️ testmontools
|
||||||
|
|
||||||
|
Лёгкий HTTP-сервис для сетевой диагностики. Оборачивает `ping`, `traceroute` и `curl` в REST API — удобно для мониторинга, дебага и автоматизации.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Содержание
|
||||||
|
|
||||||
|
- [Требования](#требования)
|
||||||
|
- [Запуск](#запуск)
|
||||||
|
- [API Reference](#api-reference)
|
||||||
|
- [POST /testmontools/ping](#post-testmontoolsping)
|
||||||
|
- [POST /testmontools/traceroute](#post-testmontoolstraceroute)
|
||||||
|
- [POST /testmontools/curl](#post-testmontoolscurl)
|
||||||
|
- [POST /testmontools/all](#post-testmontoolsall)
|
||||||
|
- [Формат входных данных](#формат-входных-данных)
|
||||||
|
- [Примеры](#примеры)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- Flask
|
||||||
|
- Утилиты `ping`, `traceroute`, `curl` в системе
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install flask
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Сервер стартует на `http://0.0.0.0:7088`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
Все эндпоинты принимают `Content-Type: application/json` и возвращают JSON.
|
||||||
|
|
||||||
|
### Формат входных данных
|
||||||
|
|
||||||
|
В теле каждого запроса передаётся одно из полей:
|
||||||
|
|
||||||
|
| Поле | Тип | Описание |
|
||||||
|
|--------|--------|--------------------------------------------|
|
||||||
|
| `url` | string | Полный URL (`https://example.com/path`) |
|
||||||
|
| `host` | string | Хост или IP (`example.com`, `8.8.8.8`) |
|
||||||
|
|
||||||
|
Если передан URL, хост извлекается автоматически там, где нужен именно он (ping, traceroute).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /testmontools/ping
|
||||||
|
|
||||||
|
Запускает `ping -c 4` до указанного хоста.
|
||||||
|
|
||||||
|
**Запрос**
|
||||||
|
```json
|
||||||
|
{ "host": "example.com" }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"input": "example.com",
|
||||||
|
"resolved_host": "example.com",
|
||||||
|
"stdout": "PING example.com (93.184.216.34) ...",
|
||||||
|
"stderr": "",
|
||||||
|
"returncode": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /testmontools/traceroute
|
||||||
|
|
||||||
|
Запускает `traceroute` до указанного хоста.
|
||||||
|
|
||||||
|
**Запрос**
|
||||||
|
```json
|
||||||
|
{ "host": "8.8.8.8" }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"input": "8.8.8.8",
|
||||||
|
"resolved_host": "8.8.8.8",
|
||||||
|
"stdout": "traceroute to 8.8.8.8 (8.8.8.8), 30 hops max ...",
|
||||||
|
"stderr": "",
|
||||||
|
"returncode": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /testmontools/curl
|
||||||
|
|
||||||
|
Выполняет `curl` к указанному URL с подробными тайминговыми метриками.
|
||||||
|
|
||||||
|
**Запрос**
|
||||||
|
```json
|
||||||
|
{ "url": "https://example.com" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Дополнительные параметры:
|
||||||
|
|
||||||
|
| Поле | Тип | Описание |
|
||||||
|
|-----------|---------|-----------------------------------------------|
|
||||||
|
| `headers` | boolean | Если `true` — запрашивает только заголовки (`-I`) |
|
||||||
|
|
||||||
|
**Ответ**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"input": "https://example.com",
|
||||||
|
"url": "https://example.com",
|
||||||
|
"timing": {
|
||||||
|
"dns_lookup": 0.012,
|
||||||
|
"tcp_connect": 0.034,
|
||||||
|
"tls_handshake":0.098,
|
||||||
|
"pre_transfer": 0.099,
|
||||||
|
"first_byte": 0.215,
|
||||||
|
"total": 0.216,
|
||||||
|
"size_bytes": 1256,
|
||||||
|
"speed_bps": 5814,
|
||||||
|
"http_code": 200
|
||||||
|
},
|
||||||
|
"trace": "* Connected to example.com ...",
|
||||||
|
"body": "<!doctype html>...",
|
||||||
|
"returncode": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Тайминговые поля**
|
||||||
|
|
||||||
|
| Поле | Описание |
|
||||||
|
|-----------------|---------------------------------------|
|
||||||
|
| `dns_lookup` | Время разрешения DNS (сек) |
|
||||||
|
| `tcp_connect` | Время установки TCP-соединения (сек) |
|
||||||
|
| `tls_handshake` | Время TLS-хендшейка (сек) |
|
||||||
|
| `pre_transfer` | Время до начала передачи (сек) |
|
||||||
|
| `first_byte` | Время до первого байта ответа (сек) |
|
||||||
|
| `total` | Общее время запроса (сек) |
|
||||||
|
| `size_bytes` | Размер скачанного тела (байт) |
|
||||||
|
| `speed_bps` | Скорость загрузки (байт/сек) |
|
||||||
|
| `http_code` | HTTP-код ответа |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /testmontools/all
|
||||||
|
|
||||||
|
Запускает `ping`, `traceroute` и `curl` **параллельно** и возвращает сводный результат.
|
||||||
|
|
||||||
|
**Запрос**
|
||||||
|
```json
|
||||||
|
{ "url": "https://example.com" }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"input": "https://example.com",
|
||||||
|
"ping": { ... },
|
||||||
|
"traceroute": { ... },
|
||||||
|
"curl": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Структура каждого вложенного объекта совпадает с ответами соответствующих отдельных эндпоинтов.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Примеры
|
||||||
|
|
||||||
|
**curl из терминала**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ping
|
||||||
|
curl -s -X POST http://localhost:7088/testmontools/ping \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"host": "google.com"}' | jq
|
||||||
|
|
||||||
|
# Только заголовки
|
||||||
|
curl -s -X POST http://localhost:7088/testmontools/curl \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"url": "https://google.com", "headers": true}' | jq
|
||||||
|
|
||||||
|
# Полная диагностика
|
||||||
|
curl -s -X POST http://localhost:7088/testmontools/all \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"url": "https://google.com"}' | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
**Python**
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
r = requests.post(
|
||||||
|
"http://localhost:7088/testmontools/all",
|
||||||
|
json={"url": "https://example.com"}
|
||||||
|
)
|
||||||
|
data = r.json()
|
||||||
|
print(data["curl"]["timing"]["first_byte"])
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Коды ответов
|
||||||
|
|
||||||
|
| HTTP-код | Описание |
|
||||||
|
|----------|-------------------------------------|
|
||||||
|
| `200` | Успешно, результат в теле ответа |
|
||||||
|
| `400` | Не передан обязательный параметр |
|
||||||
|
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
from flask import Flask, request, jsonify
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
CURL_TIMING_FORMAT = (
|
||||||
|
"dns_lookup=%(time_namelookup)s\n"
|
||||||
|
"tcp_connect=%(time_connect)s\n"
|
||||||
|
"tls_handshake=%(time_appconnect)s\n"
|
||||||
|
"pre_transfer=%(time_pretransfer)s\n"
|
||||||
|
"first_byte=%(time_starttransfer)s\n"
|
||||||
|
"total=%(time_total)s\n"
|
||||||
|
"size_bytes=%(size_download)s\n"
|
||||||
|
"speed_bps=%(speed_download)s\n"
|
||||||
|
"http_code=%(http_code)s\n"
|
||||||
|
).replace('%', '%')
|
||||||
|
|
||||||
|
CURL_NOISE = re.compile(r'[\{\}] \[\d+ bytes data\]|\[[\d\s]+ bytes data\]|\r')
|
||||||
|
|
||||||
|
|
||||||
|
def extract_host(value: str) -> str:
|
||||||
|
if value.startswith(('http://', 'https://')):
|
||||||
|
return urlparse(value).hostname or value
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def parse_input(data: dict):
|
||||||
|
raw = data.get('url') or data.get('host')
|
||||||
|
if not raw:
|
||||||
|
return None, None
|
||||||
|
return raw, extract_host(raw)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_curl_timing(text: str) -> dict:
|
||||||
|
timing = {}
|
||||||
|
for line in text.splitlines():
|
||||||
|
if '=' in line:
|
||||||
|
key, _, val = line.partition('=')
|
||||||
|
try:
|
||||||
|
timing[key.strip()] = float(val.strip())
|
||||||
|
except ValueError:
|
||||||
|
timing[key.strip()] = val.strip()
|
||||||
|
return timing
|
||||||
|
|
||||||
|
|
||||||
|
def clean_curl_output(text: str) -> str:
|
||||||
|
return '\n'.join(
|
||||||
|
line for line in text.splitlines()
|
||||||
|
if not CURL_NOISE.search(line)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ping(host: str) -> dict:
|
||||||
|
result = subprocess.run(
|
||||||
|
['ping', '-c', '4', host],
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
'resolved_host': host,
|
||||||
|
'stdout': result.stdout,
|
||||||
|
'stderr': result.stderr,
|
||||||
|
'returncode': result.returncode
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_traceroute(host: str) -> dict:
|
||||||
|
result = subprocess.run(
|
||||||
|
['traceroute', host],
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
'resolved_host': host,
|
||||||
|
'stdout': result.stdout,
|
||||||
|
'stderr': result.stderr,
|
||||||
|
'returncode': result.returncode
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def run_curl(url: str, headers_only: bool = False) -> dict:
|
||||||
|
args = [
|
||||||
|
'curl', '-sS', '-v',
|
||||||
|
'-w', '\n===TIMING===\n' + CURL_TIMING_FORMAT,
|
||||||
|
'--max-time', '30',
|
||||||
|
]
|
||||||
|
|
||||||
|
if headers_only:
|
||||||
|
args.append('-I')
|
||||||
|
|
||||||
|
args.append(url)
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
args,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Разделяем тело и timing
|
||||||
|
parts = result.stdout.split('===TIMING===')
|
||||||
|
|
||||||
|
body = parts[0].strip()
|
||||||
|
timing_text = parts[1] if len(parts) > 1 else ""
|
||||||
|
|
||||||
|
return {
|
||||||
|
'url': url,
|
||||||
|
'timing': parse_curl_timing(timing_text),
|
||||||
|
'trace': clean_curl_output(result.stderr),
|
||||||
|
'returncode': result.returncode,
|
||||||
|
'body': body
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Endpoints ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@app.route('/testmontools/ping', methods=['POST'])
|
||||||
|
def ping():
|
||||||
|
data = request.get_json()
|
||||||
|
raw, host = parse_input(data or {})
|
||||||
|
if not host:
|
||||||
|
return jsonify({'error': 'Field "url" or "host" is required'}), 400
|
||||||
|
return jsonify({'input': raw, **run_ping(host)})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/testmontools/traceroute', methods=['POST'])
|
||||||
|
def traceroute():
|
||||||
|
data = request.get_json()
|
||||||
|
raw, host = parse_input(data or {})
|
||||||
|
if not host:
|
||||||
|
return jsonify({'error': 'Field "url" or "host" is required'}), 400
|
||||||
|
return jsonify({'input': raw, **run_traceroute(host)})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/testmontools/curl', methods=['POST'])
|
||||||
|
def curl():
|
||||||
|
data = request.get_json()
|
||||||
|
raw, _ = parse_input(data or {})
|
||||||
|
if not raw:
|
||||||
|
return jsonify({'error': 'Field "url" or "host" is required'}), 400
|
||||||
|
url = raw if raw.startswith(('http://', 'https://')) else f'https://{raw}'
|
||||||
|
return jsonify({'input': raw, **run_curl(url, headers_only=bool(data.get('headers')))})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/testmontools/all', methods=['POST'])
|
||||||
|
def check_all():
|
||||||
|
data = request.get_json()
|
||||||
|
raw, host = parse_input(data or {})
|
||||||
|
if not raw or not host:
|
||||||
|
return jsonify({'error': 'Field "url" or "host" is required'}), 400
|
||||||
|
|
||||||
|
url = raw if raw.startswith(('http://', 'https://')) else f'https://{raw}'
|
||||||
|
|
||||||
|
# Запускаем ping и traceroute параллельно, curl — отдельно
|
||||||
|
with ThreadPoolExecutor(max_workers=3) as pool:
|
||||||
|
f_ping = pool.submit(run_ping, host)
|
||||||
|
f_traceroute = pool.submit(run_traceroute, host)
|
||||||
|
f_curl = pool.submit(run_curl, url)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'input': raw,
|
||||||
|
'curl': f_curl.result(),
|
||||||
|
'ping': f_ping.result(),
|
||||||
|
'traceroute': f_traceroute.result(),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=7088, debug=False)
|
||||||
Reference in New Issue
Block a user