Compare commits

...

9 Commits

Author SHA1 Message Date
65cec0a1cb jaeger event add, ip-mgmt edit
All checks were successful
Build And Test / build-and-push (push) Successful in 4m19s
2025-04-14 23:57:11 +09:00
67a3bc05c7 코드 리팩토링
All checks were successful
Build And Test / build-and-push (push) Successful in 4m30s
2025-04-14 19:11:17 +09:00
e4e6dab226 custom span for jaeger
All checks were successful
Build And Test / build-and-push (push) Successful in 4m20s
2025-04-14 15:53:23 +09:00
f8ff4ac4c7 jager endpoint change
All checks were successful
Build And Test / build-and-push (push) Successful in 4m22s
2025-04-14 15:39:49 +09:00
f860373c70 tracing add
All checks were successful
Build And Test / build-and-push (push) Successful in 4m19s
2025-04-14 15:28:57 +09:00
2796994608 create log test
All checks were successful
Build And Test / build-and-push (push) Successful in 4m22s
2025-04-14 13:30:39 +09:00
da71282a05 grafana 분리
All checks were successful
Build And Test / build-and-push (push) Successful in 5m17s
2025-04-11 11:23:34 +09:00
99a3db95a8 README.md update
All checks were successful
Build And Test / build-and-push (push) Successful in 4m21s
2025-04-10 00:23:13 +09:00
ff2e624fc8 ip mgmt view template update 2025-04-10 00:22:05 +09:00
18 changed files with 635 additions and 317 deletions

View File

@ -49,9 +49,5 @@ docker 또는 kubernetes를 이용하여 배포 합니다.
* MM_URL=https://mm.example.com/hooks/${hash}
* (기능 미구현)MINIO_URL=https://minio.example.com/
## 특이사항
### markdown에디터에 삽입된 이미지 렌더링할때 404에러가 발생하지만, 실제 동작은 잘 되는 이유.
* MinIO와 연결하여 이미지 저장 기능을 사용 할 수 있도록 구현되어 있습니다.
* Bucket을 private로 설정하는 것을 선호하므로 Presigned URL로 호출해 오도록 설정되어 있습니다.
* 게시물에는 Presigned URL이 아닌 버킷에 저장된 이미지의 이름만 들어있으므로 get_presigned_url을 이용하여 Presigned URL을 호출하기전에 렌더링이 발생하는 에러 메세지입니다.
* 에러가 아니니 기능상 문제 없이 사용 가능합니다.
## 가이드
* https://butler.icurfer.com/notice/

View File

@ -3,187 +3,183 @@
{% block title %}IP Management{% endblock %}
{% block main_area %}
<h2 class="fw-bold pt-3 pb-2">IP 관리 대장</h2>
{% if not request.user.is_authenticated %}
<p class="text-danger">비로그인 익명사용자로 접근 중입니다.
<br>로그인시 로그인 사용자가 등록한 데이터만 조회됩니다.
</p>
{% endif %}
<!-- 검색 폼 -->
<div class="row">
<div class="col-4"></div>
<div class="col-4"></div>
<div class="col-4">
<form method="get" class="mb-3">
<div class="input-group">
<input type="text" name="var_search" class="form-control" placeholder="Search by Author or Network Name..."
value="{{ var_search }}">
<button type="submit" class="btn btn-outline-primary">Search</button>
</div>
</form>
</div>
</div>
<!-- IP 레코드 목록 -->
<form id="recordForm" method="post" action="">
{% csrf_token %}
<table class="table table-striped table-hover table-bordered">
<thead class="table-dark">
<tr>
<th scope="col">Select</th>
<th scope="col">Network Name</th>
<th scope="col">Server Name</th>
<th scope="col">Location</th>
<th scope="col">IP Address</th>
<th scope="col">Remark</th>
<th scope="col">Author</th>
<th scope="col">Updated At</th>
</tr>
</thead>
<tbody>
{% for record in records %}
<tr data-record-id="{{ record.id }}">
<td class="text-center">
<input type="checkbox" name="selected_records" value="{{ record.id }}"
class="form-check-input record-checkbox">
</td>
<td>{{ record.network_nm }}</td>
<td>{{ record.contents }}</td>
<td>{{ record.svr_nm }}</td>
<td>{{ record.ip_addrs }}</td>
<td>{{ record.remark }}</td>
<td>{{ record.author }}</td>
<td>{{ record.updated_at|date:"Y-m-d H:i" }}</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center">No records found.</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if request.user.is_authenticated %}
<div class="d-flex justify-content-between align-items-center mb-3">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addDataModal">
<i class="bi bi-plus-lg"></i> Add New IP Record
</button>
<button type="submit" formaction="{% url 'butler:ip_mgmt_delete' %}" class="btn btn-danger">
<i class="bi bi-trash"></i> Delete Selected
</button>
<button type="button" id="editSelectedButton" class="btn btn-warning" disabled="disabled">
<i class="bi bi-pencil-square"></i> Edit Selected
</button>
</div>
<h2 class="fw-bold pt-3 pb-2">IP 관리 대장</h2>
{% if not request.user.is_authenticated %}
<p class="text-danger">비로그인 익명사용자로 접근 중입니다.
<br>로그인시 로그인 사용자가 등록한 데이터만 조회됩니다.
</p>
{% endif %}
</form>
<!-- 검색 폼 -->
<div class="row">
<div class="col-4"></div>
<div class="col-4"></div>
<div class="col-4">
<form method="get" class="mb-3">
<div class="input-group">
<input type="text" name="var_search" class="form-control" placeholder="Search by Author or Network Name..." value="{{ var_search }}">
<button type="submit" class="btn btn-outline-primary">Search</button>
</div>
</form>
</div>
</div>
<!-- 수정 모달들 (form 중첩 제거) -->
{% for record in records %}
<div class="modal fade" id="editDataModal-{{ record.id }}" tabindex="-1"
aria-labelledby="editDataModalLabel-{{ record.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-warning text-dark">
<h5 class="modal-title" id="editDataModalLabel-{{ record.id }}">Edit IP Record</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<!-- IP 레코드 목록 -->
<form id="recordForm" method="post" action="">
{% csrf_token %}
<table class="table table-striped table-hover table-bordered">
<thead class="table-dark">
<tr>
<th scope="col">Select</th>
<th scope="col">Network Name</th>
<th scope="col">Server Name</th>
<th scope="col">Location</th>
<th scope="col">IP Address</th>
<th scope="col">Remark</th>
<th scope="col">Author</th>
<th scope="col">Updated At</th>
</tr>
</thead>
<tbody>
{% for record in records %}
<tr data-record-id="{{ record.id }}">
<td class="text-center">
<input type="checkbox" name="selected_records" value="{{ record.id }}" class="form-check-input record-checkbox">
</td>
<td>{{ record.network_nm }}</td>
<td>{{ record.svr_nm }}</td>
<td>{{ record.contents }}</td>
<td>{{ record.ip_addrs }}</td>
<td>{{ record.remark }}</td>
<td>{{ record.author }}</td>
<td>{{ record.updated_at|date:"Y-m-d H:i" }}</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center">No records found.</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if request.user.is_authenticated %}
<div class="d-flex justify-content-between align-items-center mb-3">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addDataModal">
<i class="bi bi-plus-lg"></i>
Add New IP Record
</button>
<button type="submit" formaction="{% url 'butler:ip_mgmt_delete' %}" class="btn btn-danger">
<i class="bi bi-trash"></i>
Delete Selected
</button>
<button type="button" id="editSelectedButton" class="btn btn-warning" disabled="disabled">
<i class="bi bi-pencil-square"></i>
Edit Selected
</button>
</div>
<div class="modal-body">
<form id="editDataForm-{{ record.id }}" method="post"
action="{% url 'butler:ip_mgmt_edit' record.id %}{% if var_search %}?var_search={{ var_search }}{% endif %}">
{% csrf_token %}
{% if var_search %}<input type="hidden" name="var_search" value="{{ var_search }}">{% endif %}
<div class="mb-3">
<label for="networkNm-{{ record.id }}" class="form-label">Network Name</label>
<input type="text" class="form-control" id="networkNm-{{ record.id }}" name="network_nm"
value="{{ record.network_nm }}" required>
{% endif %}
</form>
<!-- 수정 모달들 (form 중첩 제거) -->
{% for record in records %}
<div class="modal fade" id="editDataModal-{{ record.id }}" tabindex="-1" aria-labelledby="editDataModalLabel-{{ record.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-warning text-dark">
<h5 class="modal-title" id="editDataModalLabel-{{ record.id }}">Edit IP Record</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="mb-3">
<label for="ipAddrs-{{ record.id }}" class="form-label">IP Address</label>
<input type="text" class="form-control" id="ipAddrs-{{ record.id }}" name="ip_addrs"
value="{{ record.ip_addrs }}" required>
<div class="modal-body">
<form id="editDataForm-{{ record.id }}" method="post" action="{% url 'butler:ip_mgmt_edit' record.id %}{% if var_search %}?var_search={{ var_search }}{% endif %}">
{% csrf_token %}
{% if var_search %}<input type="hidden" name="var_search" value="{{ var_search }}">{% endif %}
<div class="mb-3">
<label for="networkNm-{{ record.id }}" class="form-label">Network Name</label>
<input type="text" class="form-control" id="networkNm-{{ record.id }}" name="network_nm" value="{{ record.network_nm }}" required="required">
</div>
<div class="mb-3">
<label for="ipAddrs-{{ record.id }}" class="form-label">IP Address</label>
<input type="text" class="form-control" id="ipAddrs-{{ record.id }}" name="ip_addrs" value="{{ record.ip_addrs }}" required="required">
</div>
<div class="mb-3">
<label for="svrNm-{{ record.id }}" class="form-label">Server Name</label>
<input type="text" class="form-control" id="svrNm-{{ record.id }}" name="svr_nm" value="{{ record.svr_nm }}" required="required">
</div>
<div class="mb-3">
<label for="contents-{{ record.id }}" class="form-label">Location</label>
<textarea class="form-control" id="contents-{{ record.id }}" name="contents">{{ record.contents }}</textarea>
</div>
<div class="mb-3">
<label for="remark-{{ record.id }}" class="form-label">Remark</label>
<textarea class="form-control" id="remark-{{ record.id }}" name="remark">{{ record.remark }}</textarea>
</div>
<button type="submit" class="btn btn-primary">Save Changes</button>
</form>
</div>
<div class="mb-3">
<label for="svrNm-{{ record.id }}" class="form-label">Server Name</label>
<input type="text" class="form-control" id="svrNm-{{ record.id }}" name="svr_nm"
value="{{ record.svr_nm }}" required>
</div>
<div class="mb-3">
<label for="contents-{{ record.id }}" class="form-label">Location</label>
<textarea class="form-control" id="contents-{{ record.id }}" name="contents">{{ record.contents }}</textarea>
</div>
<div class="mb-3">
<label for="remark-{{ record.id }}" class="form-label">Remark</label>
<textarea class="form-control" id="remark-{{ record.id }}" name="remark">{{ record.remark }}</textarea>
</div>
<button type="submit" class="btn btn-primary">Save Changes</button>
</form>
</div>
</div>
</div>
{% endfor %}
<!-- 등록 모달 -->
<div class="modal fade" id="addDataModal" tabindex="-1" aria-labelledby="addDataModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title" id="addDataModalLabel">Add New IP Record</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="addDataForm" method="post" action="{% url 'butler:ip_mgmt_add' %}">
{% csrf_token %}
<div class="mb-3">
<label for="networkNm" class="form-label">Network Name</label>
<input type="text" class="form-control" id="networkNm" name="network_nm" required="required">
</div>
<div class="mb-3">
<label for="ipAddrs" class="form-label">IP Address</label>
<input type="text" class="form-control" id="ipAddrs" name="ip_addrs" required="required">
</div>
<div class="mb-3">
<label for="svrNm" class="form-label">Server Name</label>
<input type="text" class="form-control" id="svrNm" name="svr_nm" required="required">
</div>
<div class="mb-3">
<label for="contents" class="form-label">Location</label>
<textarea class="form-control" id="contents" name="contents"></textarea>
</div>
<div class="mb-3">
<label for="remark" class="form-label">Remark</label>
<textarea class="form-control" id="remark" name="remark"></textarea>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
<!-- 등록 모달 -->
<div class="modal fade" id="addDataModal" tabindex="-1" aria-labelledby="addDataModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title" id="addDataModalLabel">Add New IP Record</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="addDataForm" method="post" action="{% url 'butler:ip_mgmt_add' %}">
{% csrf_token %}
<div class="mb-3">
<label for="networkNm" class="form-label">Network Name</label>
<input type="text" class="form-control" id="networkNm" name="network_nm" required>
</div>
<div class="mb-3">
<label for="ipAddrs" class="form-label">IP Address</label>
<input type="text" class="form-control" id="ipAddrs" name="ip_addrs" required>
</div>
<div class="mb-3">
<label for="svrNm" class="form-label">Server Name</label>
<input type="text" class="form-control" id="svrNm" name="svr_nm" required>
</div>
<div class="mb-3">
<label for="contents" class="form-label">Location</label>
<textarea class="form-control" id="contents" name="contents"></textarea>
</div>
<div class="mb-3">
<label for="remark" class="form-label">Remark</label>
<textarea class="form-control" id="remark" name="remark"></textarea>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const editButton = document.getElementById('editSelectedButton');
const checkboxes = document.querySelectorAll('.record-checkbox');
<script>
document.addEventListener('DOMContentLoaded', function () {
const editButton = document.getElementById('editSelectedButton');
const checkboxes = document.querySelectorAll('.record-checkbox');
document.addEventListener('change', function () {
const selected = [...checkboxes].filter(checkbox => checkbox.checked);
if (selected.length === 1) {
editButton.disabled = false;
editButton.dataset.recordId = selected[0].value;
} else {
editButton.disabled = true;
delete editButton.dataset.recordId;
}
});
document.addEventListener('change', function () {
const selected = [...checkboxes].filter(checkbox => checkbox.checked);
if (selected.length === 1) {
editButton.disabled = false;
editButton.dataset.recordId = selected[0].value;
} else {
editButton.disabled = true;
delete editButton.dataset.recordId;
}
editButton.addEventListener('click', function () {
const recordId = editButton.dataset.recordId;
if (recordId) {
const modal = new bootstrap.Modal(document.getElementById(`editDataModal-${recordId}`));
modal.show();
}
});
});
editButton.addEventListener('click', function () {
const recordId = editButton.dataset.recordId;
if (recordId) {
const modal = new bootstrap.Modal(document.getElementById(`editDataModal-${recordId}`));
modal.show();
}
});
});
</script>
</script>
{% endblock %}

View File

@ -4,11 +4,16 @@ from django.views.generic import TemplateView
from pathlib import Path
import markdown
import os
import logging # 2025-04-14 Log 등록
from .models import IPManagementRecord
from blog.models import Post
from board_notice.models import BoardNotice
from django.db.models import Q
from opentelemetry import trace
# 2025-04-14 Log & Trace 등록
logger = logging.getLogger(__name__)
tracer = trace.get_tracer(__name__) # 트레이서 가져오기
class LandingPageView(TemplateView):
template_name = "butler/landing.html"
@ -56,7 +61,6 @@ def ip_mgmt_view(request):
records = records.order_by("ip_addrs")
return render(request, "butler/ip_mgmt.html", {"records": records, "var_search": query})
def add_ip_record(request):
if request.method == "POST":
network_nm = request.POST.get("network_nm")
@ -64,48 +68,85 @@ def add_ip_record(request):
svr_nm = request.POST.get("svr_nm")
contents = request.POST.get("contents")
remark = request.POST.get("remark")
# 작성자 (author)는 로그인된 사용자로 설정
author = request.user
# 데이터 저장
IPManagementRecord.objects.create(
network_nm=network_nm,
ip_addrs=ip_addrs,
svr_nm=svr_nm,
contents=contents,
remark=remark,
author=author,
)
# 2025-04-14 트레이스 span 생성
with tracer.start_as_current_span("create_ip_record") as span:
# 데이터 저장
IPManagementRecord.objects.create(
network_nm=network_nm,
ip_addrs=ip_addrs,
svr_nm=svr_nm,
contents=contents,
remark=remark,
author=author,
)
# 2025-04-14 Jaeger용 Event 추가
span.add_event(
"Saved IPManagementRecord",
attributes={
"ip_addrs": ip_addrs,
"svr_nm": svr_nm,
"desc": "jager logs test",
}
)
# 2025-04-14 로그 등록 (콘솔+FluentBit용)
logger.info(f"Create_Record_IP_ADDRS: {ip_addrs}")
return redirect("/ip_mgmt")
def delete_ip_records(request):
print(f"삭제동작")
# print(f"Delete_Record_IP_ADDRS")
if request.method == "POST":
selected_ids = request.POST.getlist("selected_records")
if selected_ids:
IPManagementRecord.objects.filter(id__in=selected_ids).delete()
# 2025-04-14 Log 등록
logger.info(f"Delete_Record_IP_ADDRS_idx: {selected_ids}")
return redirect("/ip_mgmt")
def edit_ip_record(request, pk):
print(f"수정동작")
# print(f"Edit_Record_IP_ADDRS")
record = get_object_or_404(IPManagementRecord, pk=pk)
# 🔥 검색 키워드 유지
# 검색 키워드 유지
var_search = request.GET.get("var_search") or request.POST.get("var_search")
if request.method == "POST":
record.network_nm = request.POST.get("network_nm")
record.ip_addrs = request.POST.get("ip_addrs")
record.svr_nm = request.POST.get("svr_nm")
record.contents = request.POST.get("contents")
record.remark = request.POST.get("remark")
record.save()
# 2025-04-14 trace span
with tracer.start_as_current_span("edit_ip_record") as span:
record.network_nm = request.POST.get("network_nm")
record.ip_addrs = request.POST.get("ip_addrs")
record.svr_nm = request.POST.get("svr_nm")
record.contents = request.POST.get("contents")
record.remark = request.POST.get("remark")
record.save()
# 2025-04-14 Jaeger용 Event 추가
span.add_event(
"Edit IPManagementRecord",
attributes={
"ip_addrs": record.ip_addrs,
"svr_nm": record.svr_nm,
"location": record.contents,
"remark": record.remark,
"desc": "jager event log insert",
}
)
# 2025-04-14 Log 등록
logger.info(f"Edit_Record_IP_ADDRS: {record.ip_addrs}")
if var_search:
return redirect(f"/ip_mgmt?var_search={var_search}")
return redirect("/ip_mgmt")
return render(request, "butler/ip_mgmt.html", {"record": record})

157
butler/views.py.bakcup Normal file
View File

@ -0,0 +1,157 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.views.generic import TemplateView
from pathlib import Path
import markdown
import os
import logging # 2025-04-14 Log 등록
from .models import IPManagementRecord
from blog.models import Post
from board_notice.models import BoardNotice
from django.db.models import Q
# 2025-04-14 Log 등록
logger = logging.getLogger(__name__)
class LandingPageView(TemplateView):
template_name = "butler/landing.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# context['var'] var를 templates로 전달해서 보여지는 것.
context['blog_posts'] = Post.objects.order_by('-created_at')[:3]
context['board_notices'] = BoardNotice.objects.order_by('-created_at')[:3]
return context
# --- ip management ---
# def ip_mgmt_view(request):
# # records = IPManagementRecord.objects.all()
# if request.user.is_authenticated:
# # records = IPManagementRecord.objects.filter(author=request.user).order_by(
# records = IPManagementRecord.objects.order_by(
# "ip_addrs"
# )
# else:
# # records = IPManagementRecord.objects.none()
# records = IPManagementRecord.objects.all().order_by("ip_addrs")
# return render(request, "butler/ip_mgmt.html", {"records": records})
def ip_mgmt_view(request):
query = request.GET.get("var_search", "").strip()
records = IPManagementRecord.objects.order_by(
"ip_addrs"
)
if query:
records = records.filter(
# Q(author__username__icontains=query) | Q(author__email__icontains=query)
Q(author__username__icontains=query) | Q(network_nm__icontains=query)
)
"""
SELECT * FROM ip_management_record AS ip JOIN auth_user AS au
ON ip.author_id = au.id
WHERE au.username LIKE CONCAT('%', 조회할 값, '%')
OR au.email LIKE CONCAT('%', 조회할 값, '%')
ORDER BY ip.ip_addrs;
"""
records = records.order_by("ip_addrs")
return render(request, "butler/ip_mgmt.html", {"records": records, "var_search": query})
def add_ip_record(request):
# print(f"Create_Record_IP_ADDRS")
if request.method == "POST":
network_nm = request.POST.get("network_nm")
ip_addrs = request.POST.get("ip_addrs")
svr_nm = request.POST.get("svr_nm")
contents = request.POST.get("contents")
remark = request.POST.get("remark")
# 작성자 (author)는 로그인된 사용자로 설정
author = request.user
# 데이터 저장
IPManagementRecord.objects.create(
network_nm=network_nm,
ip_addrs=ip_addrs,
svr_nm=svr_nm,
contents=contents,
remark=remark,
author=author,
)
# 2025-04-14 Log 등록
logger.info(f"Create_Record_IP_ADDRS: {ip_addrs}")
return redirect("/ip_mgmt")
def delete_ip_records(request):
# print(f"Delete_Record_IP_ADDRS")
if request.method == "POST":
selected_ids = request.POST.getlist("selected_records")
if selected_ids:
IPManagementRecord.objects.filter(id__in=selected_ids).delete()
# 2025-04-14 Log 등록
logger.info(f"Delete_Record_IP_ADDRS_idx: {selected_ids}")
return redirect("/ip_mgmt")
def edit_ip_record(request, pk):
# print(f"Edit_Record_IP_ADDRS")
record = get_object_or_404(IPManagementRecord, pk=pk)
# 검색 키워드 유지
var_search = request.GET.get("var_search") or request.POST.get("var_search")
if request.method == "POST":
record.network_nm = request.POST.get("network_nm")
record.ip_addrs = request.POST.get("ip_addrs")
record.svr_nm = request.POST.get("svr_nm")
record.contents = request.POST.get("contents")
record.remark = request.POST.get("remark")
record.save()
# 2025-04-14 Log 등록
logger.info(f"Edit_Record_IP_ADDRS: {record.ip_addrs}")
if var_search:
return redirect(f"/ip_mgmt?var_search={var_search}")
return redirect("/ip_mgmt")
return render(request, "butler/ip_mgmt.html", {"record": record})
# --- privacy
def privacy_view(request):
# 'docs/privacy.md' 파일을 읽기
file_path = os.path.join("docs", "docs_md_files/privacy.md")
with open(file_path, "r", encoding="utf-8") as file:
text = file.read()
file_path = Path("docs/docs_md_files/privacy.md")
with file_path.open("r", encoding="utf-8") as file:
text = file.read()
# Markdown을 HTML로 변환
# html_content = markdown.markdown(text)
# Markdown을 HTML로 변환, tables 확장 활성화
html_content = markdown.markdown(
text,
extensions=[
"tables", # 테이블 지원 확장
],
)
# 변환된 HTML을 템플릿에 전달
return render(request, "butler/privacy.html", {"content": html_content})
def test_view(request):
return render(
request,
"butler/test.html",
)

View File

@ -36,7 +36,59 @@ SECRET_KEY = 'django-insecure-fh+awf3$$^el9(#*-dpuv&++#rck@1+s=o1mx+#etv)!lpq@_5
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = int(os.environ.get('DEBUG', 1))
LOGGING = {
'version': 1,
'disable_existing_loggers': False, # Django 기본 로거 유지
'formatters': {
'standard': {
'format': '[{asctime}] {levelname} {name}:{lineno} {message}',
'style': '{',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler', # 콘솔 출력
'formatter': 'standard',
},
},
'root': {
'handlers': ['console'],
'level': 'INFO', # 기본 레벨 (애플리케이션 코드)
},
'loggers': {
'django': {
'handlers': ['console'],
'level': 'INFO', # Django 프레임워크 전반
'propagate': False,
},
'django.request': {
'handlers': ['console'],
'level': 'ERROR', # 요청 관련 에러만 (500 에러 같은 것)
'propagate': False,
},
'django.db.backends': {
'handlers': ['console'],
'level': 'WARNING', # DB 쿼리 경고만
'propagate': False,
},
'django.security': {
'handlers': ['console'],
'level': 'WARNING', # 보안 관련 경고
'propagate': False,
},
# mattermost send message log 너무 많이 나와서 조정
'apscheduler': {
'handlers': ['console'],
'level': 'WARNING', # INFO 로그 안 보이게 함 | 'CRITICAL'로 맞추면 사실상 아무것도 안 찍힘
'propagate': False,
},
},
}
# if DEBUG:
# LOGGING['loggers']['django.db.backends']['level'] = 'DEBUG'
ALLOWED_HOSTS = ["*"]

View File

@ -1,16 +1,38 @@
"""
WSGI config for butler_ddochi project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""
import os
# ✅ Django 설정을 미리 불러온다
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'butler_ddochi.settings')
from django.conf import settings # <<<< 이거 추가
# ✅ DEBUG 모드 아닐 때만 OpenTelemetry 초기화
if not settings.DEBUG:
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.django import DjangoInstrumentor
trace.set_tracer_provider(
TracerProvider(
resource=Resource.create({
"service.name": "butler_ddochi",
})
)
)
otlp_exporter = OTLPSpanExporter(
endpoint="http://jaeger-collector.istio-system:4317",
insecure=True,
)
trace.get_tracer_provider().add_span_processor(
BatchSpanProcessor(otlp_exporter)
)
DjangoInstrumentor().instrument()
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'butler_ddochi.settings')
application = get_wsgi_application()

View File

@ -0,0 +1,16 @@
"""
WSGI config for butler_ddochi project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'butler_ddochi.settings')
application = get_wsgi_application()

View File

@ -156,6 +156,10 @@
<label for="urlGrafanaEdit" class="form-label">Grafana URL</label>
<input type="url" class="form-control" id="urlGrafanaEdit" name="url_grafana" value="{{ request.user.url_grafana }}">
</div>
<div class="mb-3">
<label for="urlGrafanaDashboard_01-Edit" class="form-label">Grafana Dashboard URL</label>
<input type="url" class="form-control" id="urlGrafanaDashboard01Edit" name="url_grafana_dashboard_01" value="{{ request.user.url_grafana_dashboard_01 }}">
</div>
<div class="mb-3">
<label for="urlPrometheusEdit" class="form-label">Prometheus URL</label>
<input type="url" class="form-control" id="urlPrometheusEdit" name="url_prometheus" value="{{ request.user.url_prometheus }}">

View File

@ -28,101 +28,13 @@
</li>
{% endif %}
</ul>
<hr>
{% include "components/_sidebar_tm_dashboard.html" %}
</li>
</li>
<hr>
<li class="my-2">
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#nhnc-collapse" aria-controls="nhnc-collapse">NHN Cloud API ▽</button>
<ul class="list-unstyled ps-3 collapse" id="nhnc-collapse">
{% if request.user.is_authenticated and request.user.is_staff %}
<li>
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/test">출력결과 검토</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/preparations">주의 사항</a>
</li>
<hr>
<span>관리대장</span>
<li>
<a class="d-inline-flex align-items-center rounded" href="/nhnc_mgmt/igw">인터넷 게이트웨이</a>
</li>
<hr>
<span>Network</span>
<li>
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/infoVpcListRequest">VPC 조회 및 삭제</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/createVpcRequest">VPC 생성</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/infoClusterListRequest">NKS Cluseter 조회 및 삭제</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/createClusterOnlyRequest">NKS Cluseter 생성</a>
</li>
<hr>
<span>k8s Components deploy</span>
<li>
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/nksApply">Ingress Deploy</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/nksApply">FluentBit Deploy</a>
</li>
<li>
<a class="text-secondary d-inline-flex align-items-center rounded disabled" href="#">Logging Deploy(미구현)</a>
</li>
<li>
<a class="text-secondary d-inline-flex align-items-center rounded disabled" href="#">Monitoring Deploy(미구현)</a>
</li>
{% else %}
<p>로그인이 필요합니다.</p>
<a class="d-inline-flex align-items-center rounded" href="/notice/2/" target="_blank">사용가이드</a>
{% endif %}
</ul>
</li>
{% include "components/_sidebar_tm_dashboard.html" %}
<hr>
<li class="my-2">
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#components-collapse" aria-controls="components-collapse">DevOpsTools ▽</button>
<ul class="list-unstyled ps-3 collapse" id="components-collapse">
{% if request.user.is_authenticated and request.user.is_staff %}
<span>Dev</span>
<li>
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_gitea}}" target="_blank">Repository - Gitea</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_harbor}}" target="_blank">Registry - Harbor</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_argocd}}" target="_blank">Deploy - ArgoCD</a>
</li>
<hr>
<span>Ops</span>
<li>
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_web_ide}}" target="_blank">Web VScode - CodeServer</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_rancher}}" target="_blank">Cluster Management - Rancher</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_grafana}}" target="_blank">Monitoring - Grafana</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_prometheus}}" target="_blank">Metrics - Prometheus</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_opensearch}}" target="_blank">Container Log - OpenSearch</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_kiali}}" target="_blank">Main Cluster Traffic - Kiali</a>
</li>
{% else %}
<p>로그인이 필요합니다.</p>
<a class="d-inline-flex align-items-center rounded" href="/notice/2/" target="_blank">사용가이드</a>
{% endif %}
</ul>
</li>
{% include "components/_sidebar_nhn.html" %}
<hr>
{% include "components/_sidebar_devtools.html" %}
<hr>
<li class="my-2">
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#other-collapse" aria-controls="other-collapse">Other Tools ▽</button>

View File

@ -0,0 +1,40 @@
<li class="my-2">
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#components-collapse" aria-controls="components-collapse">DevOpsTools ▽</button>
<ul class="list-unstyled ps-3 collapse" id="components-collapse">
{% if request.user.is_authenticated and request.user.is_staff %}
<span>Dev</span>
<li>
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_gitea}}" target="_blank">Repository - Gitea</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_harbor}}" target="_blank">Registry - Harbor</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_argocd}}" target="_blank">Deploy - ArgoCD</a>
</li>
<hr>
<span>Ops</span>
<li>
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_web_ide}}" target="_blank">Web VScode - CodeServer</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_rancher}}" target="_blank">Cluster Management - Rancher</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_grafana}}" target="_blank">Monitoring - Grafana</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_prometheus}}" target="_blank">Metrics - Prometheus</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_opensearch}}" target="_blank">Container Log - OpenSearch</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_kiali}}" target="_blank">Main Cluster Traffic - Kiali</a>
</li>
{% else %}
<p>로그인이 필요합니다.</p>
{% endif %}
</ul>
</li>

View File

@ -0,0 +1,48 @@
<li class="my-2">
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#nhnc-collapse" aria-controls="nhnc-collapse">NHN Cloud API ▽</button>
<ul class="list-unstyled ps-3 collapse" id="nhnc-collapse">
{% if request.user.is_authenticated and request.user.is_staff %}
<li>
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/test">출력결과 검토</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/preparations">주의 사항</a>
</li>
<hr>
<span>관리대장</span>
<li>
<a class="d-inline-flex align-items-center rounded" href="/nhnc_mgmt/igw">인터넷 게이트웨이</a>
</li>
<hr>
<span>Network</span>
<li>
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/infoVpcListRequest">VPC 조회 및 삭제</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/createVpcRequest">VPC 생성</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/infoClusterListRequest">NKS Cluseter 조회 및 삭제</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/createClusterOnlyRequest">NKS Cluseter 생성</a>
</li>
<hr>
<span>k8s Components deploy</span>
<li>
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/nksApply">Ingress Deploy</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/nksApply">FluentBit Deploy</a>
</li>
<li>
<a class="text-secondary d-inline-flex align-items-center rounded disabled" href="#">Logging Deploy(미구현)</a>
</li>
<li>
<a class="text-secondary d-inline-flex align-items-center rounded disabled" href="#">Monitoring Deploy(미구현)</a>
</li>
{% else %}
<p>로그인이 필요합니다.</p>
{% endif %}
</ul>
</li>

View File

@ -1,8 +1,8 @@
<li class="my-2">
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#tm_dsbd-collapse" aria-controls="tm_dsbd-collapse">Telemetry Dashboard ▽</button>
<ul class="list-unstyled ps-3 collapse" id="tm_dsbd-collapse">
<li>
<a class="d-inline-flex align-items-center rounded" href="/tm_dsbd/grafana">grafana</a>
</li>
</ul>
</li>
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#tm_dsbd-collapse" aria-controls="tm_dsbd-collapse">Telemetry Dashboard ▽</button>
<ul class="list-unstyled ps-3 collapse" id="tm_dsbd-collapse">
<li>
<a class="d-inline-flex align-items-center rounded" href="/tm_dsbd/grafana">grafana</a>
</li>
</ul>
</li>

View File

@ -33,6 +33,7 @@ class CustomUserChangeForm(UserChangeForm):
'url_web_ide',
'url_rancher',
'url_grafana',
'url_grafana_dashboard_01',
'url_prometheus',
'url_opensearch',
'url_kiali',

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.14 on 2025-04-11 11:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('custom_auth', '0007_customuser_url_mattermost_customuser_url_nexus'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='url_grafana_dashboard_01',
field=models.URLField(blank=True, null=True),
),
]

View File

@ -22,12 +22,12 @@ class CustomUser(AbstractUser):
url_web_ide = models.URLField(max_length=200, blank=True, null=True)
url_rancher = models.URLField(max_length=200, blank=True, null=True)
url_grafana = models.URLField(max_length=200, blank=True, null=True)
url_grafana_dashboard_01 = models.URLField(max_length=200, blank=True, null=True) # 2025-04-11 추가
url_prometheus = models.URLField(max_length=200, blank=True, null=True)
url_opensearch = models.URLField(max_length=200, blank=True, null=True)
url_kiali = models.URLField(max_length=200, blank=True, null=True)
url_nexus = models.URLField(max_length=200, blank=True, null=True)
url_mattermost = models.URLField(max_length=200, blank=True, null=True)
def encrypt_private_key(self, private_key: str) -> bytes:
"""SSH Private Key 암호화"""

View File

@ -9,6 +9,7 @@ certifi==2024.7.4
cffi==1.16.0
charset-normalizer==3.3.2
cryptography==43.0.0
Deprecated==1.2.18
dj-rest-auth==6.0.0
Django==4.2.14
django-allauth==0.63.6
@ -18,6 +19,8 @@ django-taggit==6.0.0
djangorestframework==3.15.2
durationpy==0.9
google-auth==2.35.0
googleapis-common-protos==1.69.2
grpcio==1.71.0
gunicorn==22.0.0
idna==3.7
importlib_metadata==8.4.0
@ -31,8 +34,21 @@ mdurl==0.1.2
minio==7.2.7
mysqlclient==2.2.4
oauthlib==3.2.2
opentelemetry-api==1.32.0
opentelemetry-exporter-otlp==1.32.0
opentelemetry-exporter-otlp-proto-common==1.32.0
opentelemetry-exporter-otlp-proto-grpc==1.32.0
opentelemetry-exporter-otlp-proto-http==1.32.0
opentelemetry-instrumentation==0.53b0
opentelemetry-instrumentation-django==0.53b0
opentelemetry-instrumentation-wsgi==0.53b0
opentelemetry-proto==1.32.0
opentelemetry-sdk==1.32.0
opentelemetry-semantic-conventions==0.53b0
opentelemetry-util-http==0.53b0
packaging==24.1
pillow==10.4.0
protobuf==5.29.4
pyasn1==0.6.1
pyasn1_modules==0.4.1
pycparser==2.22
@ -52,4 +68,5 @@ tzlocal==5.2
urllib3==2.2.2
websocket-client==1.8.0
whitenoise==6.7.0
wrapt==1.17.2
zipp==3.20.0

View File

@ -7,13 +7,11 @@
<h2 class="fw-bold pb-2">Grafana</h2>
<!-- Grafana IFrame -->
<div class="ratio ratio-16x9">
<iframe
src="https://grafana.icurfer.com/public-dashboards/40d2a2615010433d81f5cf40caa03541"
width="100%"
height="800"
frameborder="0"
allowfullscreen
></iframe>
{% if request.user.url_grafana_dashboard_01 %}
<iframe src="{{request.user.url_grafana_dashboard_01}}" width="100%" height="800" frameborder="0" allowfullscreen></iframe>
{% else %}
<h3 class="text-danger">접속 후 Profile에 Dashboard URL을 등록하세요.</h3>
{% endif %}
</div>
</article>
{% endblock %}

View File

@ -1 +1 @@
dev_0.0.36
dev_0.0.44