first commit
This commit is contained in:
546
app.py
Normal file
546
app.py
Normal file
@@ -0,0 +1,546 @@
|
||||
from flask import Flask, request, redirect, url_for, render_template_string, flash, Response
|
||||
import pymysql
|
||||
from pymysql.cursors import DictCursor
|
||||
import os
|
||||
import ipaddress
|
||||
import json
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.environ.get("SECRET_KEY", "zmenit-na-vlastni-tajne-heslo")
|
||||
|
||||
DB_HOST = os.environ.get("DB_HOST", "localhost")
|
||||
DB_PORT = int(os.environ.get("DB_PORT", "3306"))
|
||||
DB_USER = os.environ.get("DB_USER", "ipam_user")
|
||||
DB_PASSWORD = os.environ.get("DB_PASSWORD", "")
|
||||
DB_NAME = os.environ.get("DB_NAME", "ipam")
|
||||
|
||||
|
||||
def get_db_connection():
|
||||
return pymysql.connect(
|
||||
host=DB_HOST,
|
||||
port=DB_PORT,
|
||||
user=DB_USER,
|
||||
password=DB_PASSWORD,
|
||||
database=DB_NAME,
|
||||
cursorclass=DictCursor,
|
||||
autocommit=True,
|
||||
charset="utf8mb4"
|
||||
)
|
||||
|
||||
|
||||
def validate_ipv4(ip_text: str) -> bool:
|
||||
try:
|
||||
ipaddress.IPv4Address(ip_text)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
INDEX_TEMPLATE = """
|
||||
<!doctype html>
|
||||
<html lang="cs">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>IPplan</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 30px;
|
||||
background: #f7f7f7;
|
||||
color: #222;
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
background: white;
|
||||
}
|
||||
th, td {
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
th {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
tr:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 8px 12px;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
background: #2563eb;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.btn:hover {
|
||||
opacity: 0.95;
|
||||
}
|
||||
.btn-secondary {
|
||||
background: #666;
|
||||
}
|
||||
.btn-export {
|
||||
background: #7c3aed;
|
||||
}
|
||||
.btn-danger {
|
||||
background: #dc2626;
|
||||
}
|
||||
.icon-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: #0f766e;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
padding: 0;
|
||||
}
|
||||
.icon-btn:hover {
|
||||
opacity: 0.95;
|
||||
}
|
||||
.flash {
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.flash.success {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
.flash.error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
.topbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 18px;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.topbar-right {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
.muted {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
input[type=text] {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.form-card {
|
||||
max-width: 700px;
|
||||
}
|
||||
.form-row {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.actions {
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.search-form {
|
||||
margin-bottom: 18px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
.search-input {
|
||||
flex: 1;
|
||||
min-width: 260px;
|
||||
}
|
||||
.ip-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.result-info {
|
||||
margin-bottom: 12px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
.empty-state {
|
||||
padding: 24px 0;
|
||||
color: #666;
|
||||
}
|
||||
.inline-form {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
async function copyToClipboard(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (err) {
|
||||
prompt("Kopírování se nepovedlo automaticky, zkopíruj ručně:", text);
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(ip) {
|
||||
return confirm("Opravdu smazat záznam pro IP " + ip + "?");
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="flash {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% if mode == 'list' %}
|
||||
<div class="topbar">
|
||||
<div>
|
||||
<h1>IPplan</h1>
|
||||
<div class="muted">Řazeno podle IP adresy</div>
|
||||
</div>
|
||||
<div class="topbar-right">
|
||||
<a class="btn btn-export" href="{{ url_for('export_json', q=query or '') }}">Export JSON</a>
|
||||
<a class="btn" href="{{ url_for('add_item') }}">+ Přidat IP</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="get" action="{{ url_for('index') }}" class="search-form">
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
name="q"
|
||||
placeholder="Hledat v IP, FQDN nebo účelu"
|
||||
value="{{ query or '' }}"
|
||||
>
|
||||
<button class="btn" type="submit">Hledat</button>
|
||||
{% if query %}
|
||||
<a class="btn btn-secondary" href="{{ url_for('index') }}">Zrušit filtr</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
<div class="result-info">
|
||||
{% if query %}
|
||||
Nalezeno záznamů: <strong>{{ rows|length }}</strong> pro výraz <strong>{{ query }}</strong>
|
||||
{% else %}
|
||||
Celkem záznamů: <strong>{{ rows|length }}</strong>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if rows %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP adresa</th>
|
||||
<th>FQDN</th>
|
||||
<th>Účel</th>
|
||||
<th>Akce</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="ip-cell">
|
||||
<span>{{ row.ipaddress }}</span>
|
||||
<button
|
||||
class="icon-btn"
|
||||
type="button"
|
||||
title="Kopírovat IP adresu"
|
||||
aria-label="Kopírovat IP adresu"
|
||||
onclick="copyToClipboard('{{ row.ipaddress }}')"
|
||||
>⧉</button>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ row.fqdn or '' }}</td>
|
||||
<td>{{ row.purpose or '' }}</td>
|
||||
<td class="actions">
|
||||
<a class="btn" href="{{ url_for('edit_item', item_id=row.id) }}">Upravit</a>
|
||||
<form
|
||||
method="post"
|
||||
action="{{ url_for('delete_item', item_id=row.id) }}"
|
||||
class="inline-form"
|
||||
onsubmit="return confirmDelete('{{ row.ipaddress }}')"
|
||||
>
|
||||
<button class="btn btn-danger" type="submit">Smazat</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="empty-state">Žádné záznamy nebyly nalezeny.</div>
|
||||
{% endif %}
|
||||
{% elif mode == 'edit' %}
|
||||
<div class="form-card">
|
||||
<h1>Upravit záznam</h1>
|
||||
|
||||
<form method="post">
|
||||
<div class="form-row">
|
||||
<label for="ipaddress">IP adresa</label>
|
||||
<input type="text" id="ipaddress" name="ipaddress" value="{{ item.ipaddress }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="fqdn">FQDN</label>
|
||||
<input type="text" id="fqdn" name="fqdn" value="{{ item.fqdn or '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="purpose">Účel</label>
|
||||
<input type="text" id="purpose" name="purpose" value="{{ item.purpose or '' }}">
|
||||
</div>
|
||||
|
||||
<button class="btn" type="submit">Uložit</button>
|
||||
<a class="btn btn-secondary" href="{{ url_for('index') }}">Zpět</a>
|
||||
</form>
|
||||
</div>
|
||||
{% elif mode == 'add' %}
|
||||
<div class="form-card">
|
||||
<h1>Přidat záznam</h1>
|
||||
|
||||
<form method="post">
|
||||
<div class="form-row">
|
||||
<label for="ipaddress">IP adresa</label>
|
||||
<input type="text" id="ipaddress" name="ipaddress" value="{{ form_data.ipaddress or '' }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="fqdn">FQDN</label>
|
||||
<input type="text" id="fqdn" name="fqdn" value="{{ form_data.fqdn or '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="purpose">Účel</label>
|
||||
<input type="text" id="purpose" name="purpose" value="{{ form_data.purpose or '' }}">
|
||||
</div>
|
||||
|
||||
<button class="btn" type="submit">Uložit</button>
|
||||
<a class="btn btn-secondary" href="{{ url_for('index') }}">Zpět</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
query = request.args.get("q", "").strip()
|
||||
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
if query:
|
||||
like_value = f"%{query}%"
|
||||
cur.execute("""
|
||||
SELECT id, ipaddress, fqdn, purpose
|
||||
FROM iplist
|
||||
WHERE ipaddress LIKE %s
|
||||
OR fqdn LIKE %s
|
||||
OR purpose LIKE %s
|
||||
ORDER BY INET_ATON(ipaddress) ASC
|
||||
""", (like_value, like_value, like_value))
|
||||
else:
|
||||
cur.execute("""
|
||||
SELECT id, ipaddress, fqdn, purpose
|
||||
FROM iplist
|
||||
ORDER BY INET_ATON(ipaddress) ASC
|
||||
""")
|
||||
rows = cur.fetchall()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return render_template_string(
|
||||
INDEX_TEMPLATE,
|
||||
mode="list",
|
||||
rows=rows,
|
||||
query=query
|
||||
)
|
||||
|
||||
|
||||
@app.route("/export/json")
|
||||
def export_json():
|
||||
query = request.args.get("q", "").strip()
|
||||
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
if query:
|
||||
like_value = f"%{query}%"
|
||||
cur.execute("""
|
||||
SELECT ipaddress, fqdn, purpose
|
||||
FROM iplist
|
||||
WHERE ipaddress LIKE %s
|
||||
OR fqdn LIKE %s
|
||||
OR purpose LIKE %s
|
||||
ORDER BY INET_ATON(ipaddress) ASC
|
||||
""", (like_value, like_value, like_value))
|
||||
else:
|
||||
cur.execute("""
|
||||
SELECT ipaddress, fqdn, purpose
|
||||
FROM iplist
|
||||
ORDER BY INET_ATON(ipaddress) ASC
|
||||
""")
|
||||
rows = cur.fetchall()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
json_data = json.dumps(rows, ensure_ascii=False, indent=2)
|
||||
|
||||
return Response(
|
||||
json_data,
|
||||
mimetype="application/json",
|
||||
headers={
|
||||
"Content-Disposition": "attachment; filename=iplist.json"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.route("/add", methods=["GET", "POST"])
|
||||
def add_item():
|
||||
form_data = {
|
||||
"ipaddress": "",
|
||||
"fqdn": "",
|
||||
"purpose": ""
|
||||
}
|
||||
|
||||
if request.method == "POST":
|
||||
form_data["ipaddress"] = request.form.get("ipaddress", "").strip()
|
||||
form_data["fqdn"] = request.form.get("fqdn", "").strip()
|
||||
form_data["purpose"] = request.form.get("purpose", "").strip()
|
||||
|
||||
if not validate_ipv4(form_data["ipaddress"]):
|
||||
flash("Neplatná IPv4 adresa.", "error")
|
||||
return render_template_string(INDEX_TEMPLATE, mode="add", form_data=form_data)
|
||||
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
try:
|
||||
cur.execute("""
|
||||
INSERT INTO iplist (ipaddress, fqdn, purpose)
|
||||
VALUES (%s, %s, %s)
|
||||
""", (
|
||||
form_data["ipaddress"],
|
||||
form_data["fqdn"] or None,
|
||||
form_data["purpose"] or None
|
||||
))
|
||||
flash("Záznam byl přidán.", "success")
|
||||
return redirect(url_for("index"))
|
||||
except pymysql.err.IntegrityError:
|
||||
flash("Tahle IP adresa už v databázi existuje.", "error")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return render_template_string(INDEX_TEMPLATE, mode="add", form_data=form_data)
|
||||
|
||||
|
||||
@app.route("/edit/<int:item_id>", methods=["GET", "POST"])
|
||||
def edit_item(item_id):
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
if request.method == "POST":
|
||||
ipaddress_value = request.form.get("ipaddress", "").strip()
|
||||
fqdn_value = request.form.get("fqdn", "").strip()
|
||||
purpose_value = request.form.get("purpose", "").strip()
|
||||
|
||||
if not validate_ipv4(ipaddress_value):
|
||||
flash("Neplatná IPv4 adresa.", "error")
|
||||
else:
|
||||
try:
|
||||
cur.execute("""
|
||||
UPDATE iplist
|
||||
SET ipaddress = %s,
|
||||
fqdn = %s,
|
||||
purpose = %s
|
||||
WHERE id = %s
|
||||
""", (
|
||||
ipaddress_value,
|
||||
fqdn_value or None,
|
||||
purpose_value or None,
|
||||
item_id
|
||||
))
|
||||
flash("Záznam byl uložen.", "success")
|
||||
return redirect(url_for("index"))
|
||||
except pymysql.err.IntegrityError:
|
||||
flash("Tahle IP adresa už v databázi existuje.", "error")
|
||||
|
||||
cur.execute("""
|
||||
SELECT id, ipaddress, fqdn, purpose
|
||||
FROM iplist
|
||||
WHERE id = %s
|
||||
""", (item_id,))
|
||||
item = cur.fetchone()
|
||||
|
||||
if not item:
|
||||
flash("Záznam nebyl nalezen.", "error")
|
||||
return redirect(url_for("index"))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return render_template_string(INDEX_TEMPLATE, mode="edit", item=item)
|
||||
|
||||
|
||||
@app.route("/delete/<int:item_id>", methods=["POST"])
|
||||
def delete_item(item_id):
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT ipaddress FROM iplist WHERE id = %s", (item_id,))
|
||||
item = cur.fetchone()
|
||||
|
||||
if not item:
|
||||
flash("Záznam nebyl nalezen.", "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
cur.execute("DELETE FROM iplist WHERE id = %s", (item_id,))
|
||||
flash(f"Záznam {item['ipaddress']} byl smazán.", "success")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5000, debug=False)
|
||||
Reference in New Issue
Block a user