Compare commits

...

5 Commits

Author SHA1 Message Date
unknown
9d5d2b8d99 Update 2025-12-19 20:23:59 2025-12-19 20:23:59 +09:00
unknown
b37c43ab86 Update 2025-12-19 19:18:16 2025-12-19 19:18:16 +09:00
unknown
b18412ecb2 Update 2025-12-19 16:23:03 2025-12-19 16:23:03 +09:00
804204ab97 update 2025-11-29 16:50:48 +09:00
25cbb6b8f8 update 2025-11-29 16:30:27 +09:00
78 changed files with 16346 additions and 1543 deletions

915
README.md

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

42
app.py
View File

@@ -46,6 +46,13 @@ setup_logging(app)
csrf = CSRFProtect()
csrf.init_app(app)
# ─────────────────────────────────────────────────────────────
# ProxyFix: Nginx/NPM 등 리버스 프록시 뒤에서 실행 시 헤더 신뢰
# (HTTPS 인식, 올바른 IP/Scheme 파악으로 CSRF/세션 문제 해결)
# ─────────────────────────────────────────────────────────────
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)
@app.context_processor
def inject_csrf():
@@ -121,38 +128,49 @@ register_routes(app, socketio)
# ─────────────────────────────────────────────────────────────
# 텔레그램 봇 폴링 서비스 (중복 실행 방지 포함)
# ─────────────────────────────────────────────────────────────
_bot_polling_started = False # 전역 플래그로 중복 방지
_bot_socket_lock = None
def start_telegram_bot_polling() -> None:
"""텔레그램 봇 폴링을 백그라운드 스레드로 시작 (한 번만 실행)"""
"""텔레그램 봇 폴링을 백그라운드 스레드로 시작 (TCP 소켓 락으로 중복 방지)"""
import threading
import socket
global _bot_polling_started
global _bot_socket_lock
if _bot_polling_started:
app.logger.warning("🤖 텔레그램 봇 폴링은 이미 시작됨 - 중복 요청 무시")
if _bot_socket_lock:
return
_bot_polling_started = True
app.logger.info("🔒 봇 중복 실행 방지 락(TCP:50000) 획득 시도...")
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("127.0.0.1", 50000))
s.listen(1)
_bot_socket_lock = s
app.logger.info("🔒 락 획득 성공! 봇 폴링 스레드를 시작합니다.")
except OSError:
app.logger.warning("⛔ 락 획득 실패: 이미 다른 프로세스(또는 좀비 프로세스)가 포트 50000을 점유 중입니다. 봇 폴링을 건너뜁니다.")
return
def _runner():
try:
# telegram_bot_service.run_polling(app) 호출
telegram_run_polling(app)
except Exception as e:
app.logger.error("텔레그램 봇 폴링 서비스 오류: %s", e)
polling_thread = threading.Thread(target=_runner, daemon=True)
polling_thread.start()
app.logger.info("🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)")
# ─────────────────────────────────────────────────────────────
# 텔레그램 봇 폴링 자동 시작
# Flask 앱이 초기화되면 자동으로 봇 폴링 시작
# 주의: Flask 리로더(Debug 모드) 사용 시 메인/워커 프로세스 중복 실행 방지
# ─────────────────────────────────────────────────────────────
start_telegram_bot_polling()
# 1. 리로더의 워커 프로세스인 경우 (WERKZEUG_RUN_MAIN = "true")
# 2. 또는 디버그 모드가 꺼진 경우 (Production)
if os.environ.get("WERKZEUG_RUN_MAIN") == "true" or not app.config.get("DEBUG"):
start_telegram_bot_polling()
# ─────────────────────────────────────────────────────────────
@@ -163,4 +181,8 @@ if __name__ == "__main__":
port = int(os.getenv("FLASK_PORT", 5000))
debug = os.getenv("FLASK_DEBUG", "true").lower() == "true"
# python app.py로 직접 실행 시(use_reloader=False)에는 위 조건문에서 실행되지 않을 수 있으므로
# 여기서 명시적으로 실행 (중복 실행 방지 플래그가 있어 안전함)
start_telegram_bot_polling()
socketio.run(app, host=host, port=port, debug=debug, allow_unsafe_werkzeug=True, use_reloader=False)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -113,14 +113,24 @@ class User(db.Model, UserMixin):
q = (email or "").strip().lower()
if not q:
return None
return User.query.filter_by(email=q).first()
try:
return User.query.filter_by(email=q).first()
except Exception as e:
logging.error(f"User find_by_email error: {e}")
db.session.rollback()
return None
@staticmethod
def find_by_username(username: Optional[str]) -> Optional["User"]:
q = (username or "").strip()
if not q:
return None
return User.query.filter_by(username=q).first()
try:
return User.query.filter_by(username=q).first()
except Exception as e:
logging.error(f"User find_by_username error: {e}")
db.session.rollback()
return None
# Flask-Login user_loader (SQLAlchemy 2.0 방식)

View File

@@ -5,7 +5,7 @@ from .auth import register_auth_routes
from .admin import register_admin_routes
from .main import register_main_routes
from .xml import register_xml_routes
from .utilities import register_util_routes
from .utilities import register_util_routes, utils_bp
from .file_view import register_file_view
from .jobs import register_jobs_routes
from .idrac_routes import register_idrac_routes
@@ -21,7 +21,7 @@ def register_routes(app: Flask, socketio=None) -> None:
register_admin_routes(app)
register_main_routes(app, socketio)
register_xml_routes(app)
register_util_routes(app)
app.register_blueprint(utils_bp, url_prefix="/utils")
register_file_view(app)
register_jobs_routes(app)
register_idrac_routes(app)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -237,3 +237,55 @@ def test_bot(bot_id):
flash(f"테스트 실패: {e}", "danger")
return redirect(url_for("admin.settings"))
# ▼▼▼ 시스템 로그 뷰어 ▼▼▼
@admin_bp.route("/admin/logs", methods=["GET"])
@login_required
@admin_required
def view_logs():
import os
import re
from collections import deque
log_folder = current_app.config.get('LOG_FOLDER')
log_file = os.path.join(log_folder, 'app.log') if log_folder else None
# 1. 실제 ANSI 이스케이프 코드 (\x1B로 시작)
ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]')
# 2. 텍스트로 찍힌 ANSI 코드 패턴 (예: [36m, [0m 등) - Werkzeug가 이스케이프 된 상태로 로그에 남길 경우 대비
literal_ansi = re.compile(r'\[[0-9;]+m')
# 3. 제어 문자 제거
control_char_re = re.compile(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]')
logs = []
if log_file and os.path.exists(log_file):
try:
with open(log_file, 'r', encoding='utf-8', errors='replace') as f:
raw_lines = deque(f, 1000)
for line in raw_lines:
# A. 실제 ANSI 코드 제거
clean_line = ansi_escape.sub('', line)
# B. 리터럴 ANSI 패턴 제거 (사용자가 [36m 등을 텍스트로 보고 있다면 이것이 원인)
clean_line = literal_ansi.sub('', clean_line)
# C. 제어 문자 제거
clean_line = control_char_re.sub('', clean_line)
# D. 앞뒤 공백 제거
clean_line = clean_line.strip()
# E. 빈 줄 제외
if clean_line:
logs.append(clean_line)
except Exception as e:
logs = [f"Error reading log file: {str(e)}"]
else:
logs = [f"Log file not found at: {log_file}"]
return render_template("admin_logs.html", logs=logs)

View File

@@ -270,8 +270,15 @@ def register():
approval_token=approval_token
)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
try:
db.session.add(user)
db.session.commit()
except Exception as e:
db.session.rollback()
current_app.logger.error("REGISTER: DB commit failed: %s", e)
flash("회원가입 처리 중 오류가 발생했습니다. (DB Error)", "danger")
return render_template("register.html", form=form)
# 텔레그램 알림 (인라인 버튼 포함)
message = (
@@ -293,6 +300,13 @@ def register():
return redirect(url_for("auth.login"))
else:
if request.method == "POST":
# 폼 검증 실패 에러를 Flash 메시지로 출력
for field_name, errors in form.errors.items():
for error in errors:
# 필드 객체 가져오기 (라벨 텍스트 확인용)
field = getattr(form, field_name, None)
label = field.label.text if field else field_name
flash(f"{label}: {error}", "warning")
current_app.logger.info("REGISTER: form errors=%s", form.errors)
return render_template("register.html", form=form)

View File

@@ -47,11 +47,43 @@ def index():
info_dir = Path(Config.IDRAC_INFO_FOLDER)
backup_dir = Path(Config.BACKUP_FOLDER)
scripts = [f.name for f in script_dir.glob("*") if f.is_file() and f.name != ".env"]
scripts = natsorted(scripts)
# 1. 스크립트 목록 조회 및 카테고리 분류
all_scripts = [f.name for f in script_dir.glob("*") if f.is_file() and f.name != ".env"]
all_scripts = natsorted(all_scripts)
grouped_scripts = {}
for script in all_scripts:
upper = script.upper()
category = "General"
if upper.startswith("GPU"):
category = "GPU"
elif upper.startswith("LOM"):
category = "LOM"
elif upper.startswith("TYPE") or upper.startswith("XE"):
category = "Server Models"
elif "MAC" in upper:
category = "MAC Info"
elif "GUID" in upper:
category = "GUID Info"
elif "SET_" in upper or "CONFIG" in upper:
category = "Configuration"
if category not in grouped_scripts:
grouped_scripts[category] = []
grouped_scripts[category].append(script)
# 카테고리 정렬 (General은 마지막에)
sorted_categories = sorted(grouped_scripts.keys())
if "General" in sorted_categories:
sorted_categories.remove("General")
sorted_categories.append("General")
grouped_scripts_sorted = {k: grouped_scripts[k] for k in sorted_categories}
# 2. XML 파일 목록
xml_files = [f.name for f in xml_dir.glob("*.xml")]
# 페이지네이션
# 3. 페이지네이션 및 파일 목록
page = int(request.args.get("page", 1))
info_files = [f.name for f in info_dir.glob("*") if f.is_file()]
info_files = natsorted(info_files)
@@ -62,11 +94,10 @@ def index():
total_pages = (len(info_files) + Config.FILES_PER_PAGE - 1) // Config.FILES_PER_PAGE
# ✅ 추가: 10개 단위로 표시될 페이지 범위 계산
start_page = ((page - 1) // 10) * 10 + 1
end_page = min(start_page + 9, total_pages)
# 백업 폴더 목록 (디렉터리만)
# 4. 백업 폴더 목록
backup_dirs = [d for d in backup_dir.iterdir() if d.is_dir()]
backup_dirs.sort(key=lambda p: p.stat().st_mtime, reverse=True)
@@ -91,7 +122,8 @@ def index():
backup_files=backup_files,
total_backup_pages=total_backup_pages,
backup_page=backup_page,
scripts=scripts,
scripts=all_scripts, # 기존 리스트 호환
grouped_scripts=grouped_scripts_sorted, # 카테고리별 분류
xml_files=xml_files,
)
@@ -175,13 +207,13 @@ def delete_file(filename: str):
if file_path.exists():
try:
file_path.unlink()
flash(f"{filename} 삭제됨.")
flash(f"'{filename}' 파일이 삭제되었습니다.", "success")
logging.info(f"파일 삭제됨: {filename}")
except Exception as e:
logging.error(f"파일 삭제 오류: {e}")
flash("파일 삭제 중 오류가 발생했습니다.", "danger")
else:
flash("파일이 존재하지 않습니다.")
flash("파일이 존재하지 않습니다.", "warning")
return redirect(url_for("main.index"))

View File

@@ -32,26 +32,48 @@ def diff_scp():
return redirect(url_for("xml.xml_management"))
# 파일 내용 읽기 (LF로 통일)
content1 = file1_path.read_text(encoding="utf-8").replace("\r\n", "\n").splitlines()
content2 = file2_path.read_text(encoding="utf-8").replace("\r\n", "\n").splitlines()
# 파일 내용 읽기 (LF로 통일)
# Monaco Editor에 원본 텍스트를 그대로 전달하기 위해 splitlines() 제거
# 파일 내용 읽기 (LF로 통일)
logger.info(f"Reading file1: {file1_path}")
content1 = file1_path.read_text(encoding="utf-8", errors="replace").replace("\r\n", "\n")
# Diff 생성
diff = difflib.unified_diff(
content1, content2,
fromfile=file1_name,
tofile=file2_name,
lineterm=""
)
logger.info(f"Reading file2: {file2_path}")
content2 = file2_path.read_text(encoding="utf-8", errors="replace").replace("\r\n", "\n")
diff_content = "\n".join(diff)
logger.info(f"Content1 length: {len(content1)}, Content2 length: {len(content2)}")
return render_template("scp_diff.html", file1=file1_name, file2=file2_name, diff_content=diff_content)
return render_template("scp_diff.html",
file1=file1_name,
file2=file2_name,
content1=content1,
content2=content2)
except Exception as e:
logger.error(f"Diff error: {e}")
flash(f"비교 중 오류가 발생했습니다: {str(e)}", "danger")
return redirect(url_for("xml.xml_management"))
@scp_bp.route("/scp/content/<path:filename>")
@login_required
def get_scp_content(filename):
"""
XML 파일 내용을 반환하는 API (Monaco Editor용)
"""
try:
safe_name = sanitize_preserve_unicode(filename)
path = Path(Config.XML_FOLDER) / safe_name
if not path.exists():
return "File not found", 404
# 텍스트로 읽어서 반환
content = path.read_text(encoding="utf-8", errors="replace").replace("\r\n", "\n")
return content, 200, {'Content-Type': 'text/plain; charset=utf-8'}
except Exception as e:
logger.error(f"Content read error: {e}")
return str(e), 500
@scp_bp.route("/scp/export", methods=["POST"])
@login_required
def export_scp():

View File

@@ -290,13 +290,83 @@ def update_gpu_list():
return redirect(url_for("main.index"))
@utils_bp.route("/download_excel")
@login_required
def download_excel():
path = Path(Config.SERVER_LIST_FOLDER) / "mac_info.xlsx"
if not path.is_file():
flash("엑셀 파일을 찾을 수 없습니다.", "danger")
return redirect(url_for("main.index"))
logging.info(f"엑셀 파일 다운로드: {path}")
return send_file(str(path), as_attachment=True, download_name="mac_info.xlsx")
@utils_bp.route("/scan_network", methods=["POST"])
@login_required
def scan_network():
"""
지정된 IP 범위(Start ~ End)에 대해 Ping 테스트를 수행하고
응답이 있는 IP 목록을 반환합니다.
"""
try:
import ipaddress
import platform
import concurrent.futures
data = request.get_json(force=True, silent=True) or {}
start_ip_str = data.get('start_ip')
end_ip_str = data.get('end_ip')
if not start_ip_str or not end_ip_str:
return jsonify({"success": False, "error": "시작 IP와 종료 IP를 모두 입력해주세요."}), 400
try:
start_ip = ipaddress.IPv4Address(start_ip_str)
end_ip = ipaddress.IPv4Address(end_ip_str)
if start_ip > end_ip:
return jsonify({"success": False, "error": "시작 IP가 종료 IP보다 큽니다."}), 400
# IP 개수 제한 (너무 많은 스캔 방지, 예: C클래스 2개 분량 512개)
if int(end_ip) - int(start_ip) > 512:
return jsonify({"success": False, "error": "스캔 범위가 너무 넓습니다. (최대 512개)"}), 400
except ValueError:
return jsonify({"success": False, "error": "유효하지 않은 IP 주소 형식입니다."}), 400
# Ping 함수 정의
def ping_ip(ip_obj):
ip = str(ip_obj)
param = '-n' if platform.system().lower() == 'windows' else '-c'
timeout_param = '-w' if platform.system().lower() == 'windows' else '-W'
# Windows: -w 200 (ms), Linux: -W 1 (s)
timeout_val = '200' if platform.system().lower() == 'windows' else '1'
command = ['ping', param, '1', timeout_param, timeout_val, ip]
try:
# shell=False로 보안 강화, stdout/stderr 무시
res = subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return ip if res.returncode == 0 else None
except Exception:
return None
active_ips = []
# IP 리스트 생성
target_ips = []
temp_ip = start_ip
while temp_ip <= end_ip:
target_ips.append(temp_ip)
temp_ip += 1
# 병렬 처리 (최대 50 쓰레드)
with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor:
results = executor.map(ping_ip, target_ips)
# 결과 수집 (None 제외)
active_ips = [ip for ip in results if ip is not None]
return jsonify({
"success": True,
"active_ips": active_ips,
"count": len(active_ips),
"message": f"스캔 완료: {len(active_ips)}개의 활성 IP 발견"
})
except Exception as e:
logging.error(f"Scan network fatal error: {e}")
return jsonify({"success": False, "error": f"서버 내부 오류: {str(e)}"}), 500

Binary file not shown.

View File

@@ -60,6 +60,19 @@ def setup_logging(app: Optional[object] = None) -> logging.Logger:
# Flask 앱 로거에도 동일 핸들러 바인딩
app.logger.handlers = root.handlers
app.logger.setLevel(root.level)
# 루트 로거로 전파되면 메시지가 두 번 출력되므로 방지
app.logger.propagate = False
# 제3자 라이브러리 로그 레벨 조정 (너무 시끄러운 경우)
# werkzeug: 기본적인 HTTP 요청 로그(GET/POST 등)를 숨김 (WARNING 이상만 표시)
logging.getLogger("werkzeug").setLevel(logging.WARNING)
logging.getLogger("socketio").setLevel(logging.WARNING)
logging.getLogger("engineio").setLevel(logging.WARNING)
# httpx, telegram 라이브러리의 HTTP 요청 로그 숨기기
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
logging.getLogger("telegram").setLevel(logging.WARNING)
root.info("Logger initialized | level=%s | file=%s", _DEF_LEVEL, log_path)
return root

View File

@@ -1,5 +1,7 @@
document.addEventListener('DOMContentLoaded', () => {
// ─────────────────────────────────────────────────────────────
// 스크립트 선택 시 XML 드롭다운 토글
// ─────────────────────────────────────────────────────────────
@@ -77,6 +79,37 @@ document.addEventListener('DOMContentLoaded', () => {
const csrfToken = document.querySelector('input[name="csrf_token"]')?.value || '';
// ─────────────────────────────────────────────────────────────
// IP 입력 데이터 보존 (Local Storage)
// ─────────────────────────────────────────────────────────────
const ipTextarea = document.getElementById('ips');
const ipForm = document.getElementById('ipForm');
const STORAGE_KEY_IP = 'ip_input_draft';
if (ipTextarea) {
// 1. 페이지 로드 시 저장된 값 복원
const savedIps = localStorage.getItem(STORAGE_KEY_IP);
if (savedIps) {
ipTextarea.value = savedIps;
// 라인 수 업데이트 트리거
if (window.updateIpCount) window.updateIpCount();
}
// 2. 입력 시마다 저장
ipTextarea.addEventListener('input', () => {
localStorage.setItem(STORAGE_KEY_IP, ipTextarea.value);
// script.js에 있는 updateIpCount 호출 (있다면)
if (window.updateIpCount) window.updateIpCount();
});
// 3. 폼 제출 성공 시 초기화?
// 사용자의 의도에 따라 다름: "변경이 되지 않는 이상 계속 가지고 있게"
// -> 제출 후에도 유지하는 것이 요청 사항에 부합함.
// 만약 '성공적으로 작업이 끝나면 지워달라'는 요청이 있으면 여기를 수정.
// 현재 요청: "페이지가 리셋이되도 변경이 되지 않는이상 계속 가지고있게" -> 유지.
}
// ─────────────────────────────────────────────────────────────
// 공통 POST 함수
// ─────────────────────────────────────────────────────────────
@@ -119,7 +152,7 @@ document.addEventListener('DOMContentLoaded', () => {
await postFormAndHandle(macForm.action);
location.reload();
} catch (err) {
alert('MAC 이동 중 오류: ' + (err?.message || err));
alert('MAC 파일 이동 중 오류가 발생했습니다: ' + (err?.message || err));
btn.disabled = false;
btn.innerHTML = originalHtml;
}
@@ -142,7 +175,7 @@ document.addEventListener('DOMContentLoaded', () => {
await postFormAndHandle(guidForm.action);
location.reload();
} catch (err) {
alert('GUID 이동 중 오류: ' + (err?.message || err));
alert('GUID 파일 이동 중 오류가 발생했습니다: ' + (err?.message || err));
btn.disabled = false;
btn.innerHTML = originalHtml;
}
@@ -160,4 +193,106 @@ document.addEventListener('DOMContentLoaded', () => {
});
}, 5000);
// ─────────────────────────────────────────────────────────────
// IP 스캔 로직 (Modal)
// ─────────────────────────────────────────────────────────────
const btnScan = document.getElementById('btnStartScan');
if (btnScan) {
btnScan.addEventListener('click', async () => {
const startIp = '10.10.0.2';
const endIp = '10.10.0.255';
const ipsTextarea = document.getElementById('ips');
const progressBar = document.getElementById('progressBar');
// UI 상태 변경 (로딩 중)
const originalIcon = btnScan.innerHTML;
btnScan.disabled = true;
btnScan.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>';
// 메인 진행바 활용
if (progressBar) {
const progressContainer = progressBar.closest('.progress');
if (progressContainer) {
progressContainer.parentElement.classList.remove('d-none');
}
progressBar.style.width = '100%';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
progressBar.textContent = 'IP 스캔 중...';
}
try {
const res = await fetch('/utils/scan_network', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ start_ip: startIp, end_ip: endIp })
});
// 1. 세션 만료로 인한 리다이렉트 감지
if (res.redirected) {
alert('세션이 만료되었습니다. 다시 로그인해주세요.');
window.location.reload();
return;
}
// 2. JSON 응답인지 확인
const contentType = res.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
const text = await res.text();
if (text.includes("CSRF")) {
throw new Error("보안 토큰(CSRF)이 만료되었습니다. 페이지를 새로고침해주세요.");
}
throw new Error(`서버 응답 오류 (HTTP ${res.status}): ${text.substring(0, 100)}...`);
}
const data = await res.json();
if (data.success) {
if (data.active_ips && data.active_ips.length > 0) {
ipsTextarea.value = data.active_ips.join('\n');
// 이벤트 트리거
ipsTextarea.dispatchEvent(new Event('input'));
alert(`스캔 완료: ${data.active_ips.length}개의 활성 IP를 찾았습니다.`);
} else {
alert('활성 IP를 발견하지 못했습니다.');
}
} else {
throw new Error(data.error || 'Unknown error');
}
} catch (err) {
console.error(err);
alert('오류가 발생했습니다: ' + (err.message || err));
} finally {
// 상태 복구
btnScan.disabled = false;
btnScan.innerHTML = originalIcon;
if (progressBar) {
// 진행바 초기화
progressBar.style.width = '0%';
progressBar.textContent = '0%';
progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
}
}
});
}
// ─────────────────────────────────────────────────────────────
// IP 입력 지우기 버튼
// ─────────────────────────────────────────────────────────────
const btnClear = document.getElementById('btnClearIps');
if (btnClear) {
btnClear.addEventListener('click', () => {
const ipsTextarea = document.getElementById('ips');
if (ipsTextarea) {
ipsTextarea.value = '';
ipsTextarea.dispatchEvent(new Event('input')); // 로컬 스토리지 업데이트 및 카운트 갱신 트리거
}
});
}
});

View File

@@ -401,7 +401,7 @@ document.addEventListener('DOMContentLoaded', async () => {
if (monitoringOn) await fetchJobs(false);
}
} catch (e) {
alert('IP 목록 불러오기 실패: ' + e.message);
alert('IP 목록 불러오는 중 오류가 발생했습니다: ' + e.message);
}
});
$btnApply.addEventListener('click', () => {

View File

@@ -1,72 +1,151 @@
{# backend/templates/admin.html #}
{% extends "base.html" %}
{% block title %}관리자 패널 - Dell Server Info{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="card-title mb-0">Admin Page</h3>
<a href="{{ url_for('admin.settings') }}" class="btn btn-outline-primary">
<i class="bi bi-gear-fill me-1"></i>시스템 설정
</a>
</div>
<div class="container py-4">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-1">
<i class="bi bi-shield-lock text-primary me-2"></i>관리자 패널
</h2>
<p class="text-muted mb-0">사용자 관리 및 시스템 설정을 수행합니다.</p>
</div>
<div class="d-flex gap-2">
<a href="{{ url_for('admin.view_logs') }}" class="btn btn-outline-secondary">
<i class="bi bi-journal-text me-1"></i>로그 보기
</a>
<a href="{{ url_for('admin.settings') }}" class="btn btn-primary">
<i class="bi bi-gear-fill me-1"></i>시스템 설정
</a>
</div>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="mt-2">
{% for cat, msg in messages %}
<div class="alert alert-{{ cat }} alert-dismissible fade show" role="alert">
{{ msg }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<!-- Dashboard Stats -->
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="card border-0 shadow-sm h-100 bg-primary bg-opacity-10">
<div class="card-body d-flex align-items-center">
<div class="rounded-circle bg-primary text-white p-3 me-3">
<i class="bi bi-people-fill fs-4"></i>
</div>
<div>
<h6 class="text-primary fw-bold mb-1">총 사용자</h6>
<h3 class="mb-0 fw-bold">{{ users|length }}</h3>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm h-100 bg-success bg-opacity-10">
<div class="card-body d-flex align-items-center">
<div class="rounded-circle bg-success text-white p-3 me-3">
<i class="bi bi-person-check-fill fs-4"></i>
</div>
<div>
<h6 class="text-success fw-bold mb-1">활성 사용자</h6>
{% set active_users = users | selectattr("is_active") | list %}
<h3 class="mb-0 fw-bold">{{ active_users|length }}</h3>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm h-100 bg-warning bg-opacity-10">
<div class="card-body d-flex align-items-center">
<div class="rounded-circle bg-warning text-white p-3 me-3">
<i class="bi bi-person-dash-fill fs-4"></i>
</div>
<div>
<h6 class="text-warning fw-bold mb-1">승인 대기</h6>
<h3 class="mb-0 fw-bold">{{ (users|length) - (active_users|length) }}</h3>
</div>
</div>
</div>
</div>
</div>
<!-- User Management Table -->
<div class="card border shadow-sm">
<div class="card-header bg-white border-bottom py-3">
<h5 class="mb-0 fw-bold">
<i class="bi bi-person-lines-fill text-primary me-2"></i>사용자 목록
</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-striped align-middle">
<thead>
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th style="width:60px">ID</th>
<th>Username</th>
<th>Email</th>
<th style="width:80px">Active</th>
<th style="width:260px">Action</th>
<th class="ps-4 py-3 text-secondary text-uppercase small fw-bold" style="width: 60px;">NO</th>
<th class="py-3 text-secondary text-uppercase small fw-bold">이름</th>
<th class="py-3 text-secondary text-uppercase small fw-bold">ID (Email)</th>
<th class="py-3 text-secondary text-uppercase small fw-bold">상태</th>
<th class="py-3 text-secondary text-uppercase small fw-bold text-end pe-4">관리</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td class="ps-4 fw-bold text-secondary">{{ loop.index }}</td>
<td>
<div class="d-flex align-items-center">
<div
class="avatar-initial rounded-circle bg-light text-primary fw-bold me-2 d-flex align-items-center justify-content-center border"
style="width: 32px; height: 32px; font-size: 0.9rem;">
{{ user.username[:1] | upper }}
</div>
<span class="fw-bold text-dark">{{ user.username }}</span>
</div>
</td>
<td class="text-secondary small font-monospace">{{ user.email }}</td>
<td>
{% if user.is_active %}
<span class="badge bg-success">Yes</span>
<span class="badge bg-success-subtle text-success border border-success-subtle rounded-pill px-3">
<i class="bi bi-check-circle-fill me-1"></i>Active
</span>
{% else %}
<span class="badge bg-secondary">No</span>
<span class="badge bg-warning-subtle text-warning border border-warning-subtle rounded-pill px-3">
<i class="bi bi-hourglass-split me-1"></i>Pending
</span>
{% endif %}
{% if user.is_admin %}
<span
class="badge bg-primary-subtle text-primary border border-primary-subtle rounded-pill px-2 ms-1">Admin</span>
{% endif %}
</td>
<td>
{% if not user.is_active %}
<a href="{{ url_for('admin.approve_user', user_id=user.id) }}"
class="btn btn-success btn-sm me-1">Approve</a>
{% endif %}
<a href="{{ url_for('admin.delete_user', user_id=user.id) }}" class="btn btn-danger btn-sm me-1"
onclick="return confirm('사용자 {{ user.username }} (ID={{ user.id }}) 를 삭제하시겠습니까?');">
Delete
</a>
<td class="text-end pe-4">
<div class="d-flex justify-content-end gap-2">
{% if not user.is_active %}
<a href="{{ url_for('admin.approve_user', user_id=user.id) }}"
class="btn btn-sm btn-success text-white d-flex align-items-center gap-1" title="가입 승인">
<i class="bi bi-check-lg"></i>승인
</a>
{% endif %}
<!-- Change Password 버튼: 모달 오픈 -->
<button type="button" class="btn btn-primary btn-sm" data-user-id="{{ user.id }}"
data-username="{{ user.username | e }}" data-bs-toggle="modal" data-bs-target="#changePasswordModal">
Change Password
</button>
<button type="button" class="btn btn-sm btn-outline-secondary d-flex align-items-center gap-1"
data-user-id="{{ user.id }}" data-username="{{ user.username | e }}" data-bs-toggle="modal"
data-bs-target="#changePasswordModal">
<i class="bi bi-key"></i>비밀번호
</button>
<a href="{{ url_for('admin.delete_user', user_id=user.id) }}"
class="btn btn-sm btn-outline-danger d-flex align-items-center gap-1"
onclick="return confirm('⚠️ 경고: 사용자 [{{ user.username }}]님을 정말 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.');">
<i class="bi bi-trash"></i>삭제
</a>
</div>
</td>
</tr>
{% endfor %}
{% if not users %}
<tr>
<td colspan="4" class="text-center py-5 text-muted">사용자가 없습니다.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
@@ -75,41 +154,44 @@
</div>
{# ========== Change Password Modal ========== #}
<div class="modal fade" id="changePasswordModal" tabindex="-1" aria-labelledby="changePasswordModalLabel"
aria-hidden="true">
<div class="modal fade" id="changePasswordModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-content border-0 shadow">
<form id="changePasswordForm" method="post" action="">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="modal-header">
<h5 class="modal-title" id="changePasswordModalLabel">Change Password</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="modal-header bg-light">
<h5 class="modal-title fw-bold">
<i class="bi bi-key-fill me-2"></i>비밀번호 변경
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-2">
<small class="text-muted">User:</small>
<div id="modalUserInfo" class="fw-bold"></div>
<div class="modal-body p-4">
<div class="alert alert-light border mb-4 d-flex align-items-center">
<i class="bi bi-person-circle fs-4 me-3 text-secondary"></i>
<div>
<small class="text-muted d-block">대상 사용자</small>
<span id="modalUserInfo" class="fw-bold text-dark fs-5"></span>
</div>
</div>
<div class="mb-3">
<label for="newPasswordInput" class="form-label">New password</label>
<label for="newPasswordInput" class="form-label fw-semibold">새 비밀번호</label>
<input id="newPasswordInput" name="new_password" type="password" class="form-control" required minlength="8"
placeholder="Enter new password">
<div class="form-text">최소 8자 이상을 권장합니다.</div>
placeholder="최소 8자 이상">
</div>
<div class="mb-3">
<label for="confirmPasswordInput" class="form-label">Confirm password</label>
<label for="confirmPasswordInput" class="form-label fw-semibold">비밀번호 확인</label>
<input id="confirmPasswordInput" name="confirm_password" type="password" class="form-control" required
minlength="8" placeholder="Confirm new password">
minlength="8" placeholder="비밀번호 재입력">
<div id="pwMismatch" class="invalid-feedback">비밀번호가 일치하지 않습니다.</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button id="modalSubmitBtn" type="submit" class="btn btn-primary">Change Password</button>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
<button id="modalSubmitBtn" type="submit" class="btn btn-primary px-4">변경 저장</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,245 @@
{% extends "base.html" %}
{% block title %}시스템 로그 - Dell Server Info{% endblock %}
{% block extra_css %}
<style>
/* 전체 레이아웃 */
.editor-container {
display: flex;
flex-direction: column;
height: 600px;
background: #1e1e1e;
border: 1px solid #333;
border-radius: 6px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
/* 툴바 (헤더) */
.editor-toolbar {
background-color: #252526;
border-bottom: 1px solid #333;
padding: 0.5rem 1rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
}
/* 에디터 본문 */
#monaco-editor-root {
flex: 1;
width: 100%;
height: 100%;
}
/* 로딩 인디케이터 */
.editor-loading {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #d4d4d4;
font-size: 1.1rem;
background: #1e1e1e;
}
</style>
{% endblock %}
{% block content %}
<div class="container py-4">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h2 class="fw-bold mb-1">
<i class="bi bi-terminal text-dark me-2"></i>시스템 로그
</h2>
<p class="text-muted mb-0 small">최근 생성된 1000줄의 시스템 로그를 실시간으로 확인합니다.</p>
</div>
<div>
<a href="{{ url_for('admin.admin_panel') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>돌아가기
</a>
</div>
</div>
<div class="editor-container">
<!-- Toolbar -->
<div class="editor-toolbar">
<div class="d-flex gap-2 align-items-center flex-wrap">
<div class="input-group input-group-sm" style="width: 250px;">
<span class="input-group-text bg-dark border-secondary text-light"><i
class="bi bi-search"></i></span>
<input type="text" id="logSearch" class="form-control bg-dark border-secondary text-light"
placeholder="검색어 입력...">
</div>
<div class="btn-group btn-group-sm" role="group">
<input type="checkbox" class="btn-check" id="checkInfo" checked autocomplete="off">
<label class="btn btn-outline-secondary text-light" for="checkInfo">INFO</label>
<input type="checkbox" class="btn-check" id="checkWarn" checked autocomplete="off">
<label class="btn btn-outline-warning" for="checkWarn">WARN</label>
<input type="checkbox" class="btn-check" id="checkError" checked autocomplete="off">
<label class="btn btn-outline-danger" for="checkError">ERROR</label>
</div>
</div>
<div class="d-flex gap-2">
<button class="btn btn-sm btn-outline-light" id="btnScrollBottom">
<i class="bi bi-arrow-down-circle me-1"></i>맨 아래로
</button>
<a href="{{ url_for('admin.view_logs') }}" class="btn btn-primary btn-sm">
<i class="bi bi-arrow-clockwise me-1"></i>새로고침
</a>
</div>
</div>
<!-- Editor Area -->
<div id="monaco-editor-root">
<div class="editor-loading">
<div class="spinner-border text-light me-3" role="status"></div>
<div>로그 뷰어를 불러오는 중...</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<!-- Monaco Editor Loader -->
<script src="https://unpkg.com/monaco-editor@0.45.0/min/vs/loader.js"></script>
<script>
// 서버에서 전달된 로그 데이터 (Python list -> JS array)
// tojson safe 필터 사용
const allLogs = {{ logs | tojson | safe }};
document.addEventListener('DOMContentLoaded', function () {
if (typeof require === 'undefined') {
document.querySelector('.editor-loading').innerHTML =
'<div class="text-danger"><i class="bi bi-exclamation-triangle me-2"></i>Monaco Editor를 로드할 수 없습니다.</div>';
return;
}
require.config({ paths: { 'vs': 'https://unpkg.com/monaco-editor@0.45.0/min/vs' } });
require(['vs/editor/editor.main'], function () {
var container = document.getElementById('monaco-editor-root');
container.innerHTML = ''; // 로딩 제거
// 1. 커스텀 로그 언어 정의 (간단한 하이라이팅)
monaco.languages.register({ id: 'simpleLog' });
monaco.languages.setMonarchTokensProvider('simpleLog', {
tokenizer: {
root: [
[/\[INFO\]|INFO:/, 'info-token'],
[/\[WARNING\]|\[WARN\]|WARNING:|WARN:/, 'warn-token'],
[/\[ERROR\]|ERROR:|Traceback/, 'error-token'],
[/\[DEBUG\]|DEBUG:/, 'debug-token'],
[/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}/, 'date-token'],
[/".*?"/, 'string']
]
}
});
// 2. 테마 정의
monaco.editor.defineTheme('logTheme', {
base: 'vs-dark',
inherit: true,
rules: [
{ token: 'info-token', foreground: '4ec9b0' },
{ token: 'warn-token', foreground: 'cca700', fontStyle: 'bold' },
{ token: 'error-token', foreground: 'f44747', fontStyle: 'bold' },
{ token: 'debug-token', foreground: '808080' },
{ token: 'date-token', foreground: '569cd6' },
],
colors: {
'editor.background': '#1e1e1e'
}
});
// 3. 에디터 생성
var editor = monaco.editor.create(container, {
value: allLogs.join('\n'),
language: 'simpleLog',
theme: 'logTheme',
readOnly: true,
automaticLayout: true,
minimap: { enabled: true },
fontSize: 13,
lineHeight: 19, // 밀도 조절
scrollBeyondLastLine: false,
lineNumbers: 'on',
wordWrap: 'on',
renderLineHighlight: 'all',
contextmenu: false,
padding: { top: 10, bottom: 10 }
});
// 4. 필터링 로직
function updateLogs() {
const query = document.getElementById('logSearch').value.toLowerCase();
const showInfo = document.getElementById('checkInfo').checked;
const showWarn = document.getElementById('checkWarn').checked;
const showError = document.getElementById('checkError').checked;
const filtered = allLogs.filter(line => {
const lower = line.toLowerCase();
// 레벨 체크 (매우 단순화)
let levelMatch = false;
const isError = lower.includes('[error]') || lower.includes('error:') || lower.includes('traceback');
const isWarn = lower.includes('[warning]') || lower.includes('[warn]') || lower.includes('warn:');
const isInfo = lower.includes('[info]') || lower.includes('info:');
if (isError) {
if (showError) levelMatch = true;
} else if (isWarn) {
if (showWarn) levelMatch = true;
} else if (isInfo) {
if (showInfo) levelMatch = true;
} else {
// 레벨 키워드가 없는 줄은 기본적으로 표시 (맥락 유지)
levelMatch = true;
}
if (!levelMatch) return false;
// 검색어 체크
if (query && !lower.includes(query)) return false;
return true;
});
// 현재 스크롤 위치 저장? 아니면 항상 아래로? -> 보통 필터링하면 아래로 가는게 편함
const currentModel = editor.getModel();
if (currentModel) {
currentModel.setValue(filtered.join('\n'));
}
// editor.revealLine(editor.getModel().getLineCount());
}
// 이벤트 연결
document.getElementById('logSearch').addEventListener('keyup', updateLogs);
document.getElementById('checkInfo').addEventListener('change', updateLogs);
document.getElementById('checkWarn').addEventListener('change', updateLogs);
document.getElementById('checkError').addEventListener('change', updateLogs);
// 맨 아래로 버튼
document.getElementById('btnScrollBottom').addEventListener('click', function () {
editor.revealLine(editor.getModel().getLineCount());
});
// 초기 스크롤 (약간의 지연 후)
setTimeout(() => {
editor.revealLine(editor.getModel().getLineCount());
}, 100);
});
});
</script>
{% endblock %}

View File

@@ -240,81 +240,89 @@
<h5 class="text-muted fw-normal">등록된 텔레그램 봇이 없습니다.</h5>
<p class="text-muted small mb-4">우측 상단의 '봇 추가' 버튼을 눌러 알림을 설정하세요.</p>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addBotModal">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<form action="{{ url_for('admin.add_bot') }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="modal-header bg-light">
<h5 class="modal-title fw-bold">
<i class="bi bi-plus-circle me-2"></i>새 텔레그램 봇 추가
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-semibold">이름 (식별용)</label>
<input type="text" class="form-control" name="name" placeholder="예: 알림용 봇"
required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Bot Token</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-key"></i></span>
<input type="text" class="form-control font-monospace" name="token"
placeholder="123456:ABC..." required>
</div>
<div class="form-text">BotFather에게 받은 API Token</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Chat ID</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-chat-dots"></i></span>
<input type="text" class="form-control font-monospace" name="chat_id"
placeholder="-100..." required>
</div>
<div class="form-text">메시지를 받을 채팅방 ID (그룹은 음수)</div>
</div>
<i class="bi bi-plus-lg me-1"></i>봇 추가
</button>
</div>
{% endif %}
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">알림 유형</label>
<div class="d-flex gap-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="notify_types"
value="auth" id="add_auth" checked>
<label class="form-check-label" for="add_auth">
인증
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="notify_types"
value="activity" id="add_activity" checked>
<label class="form-check-label" for="add_activity">
활동
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="notify_types"
value="system" id="add_system" checked>
<label class="form-check-label" for="add_system">
시스템
</label>
</div>
</div>
<small class="text-muted">선택한 알림 유형만 전송됩니다</small>
</div>
<!-- 봇 추가 모달 -->
<div class="modal fade" id="addBotModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<form action="{{ url_for('admin.add_bot') }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="modal-header bg-light">
<h5 class="modal-title fw-bold">
<i class="bi bi-plus-circle me-2"></i>새 텔레그램 봇 추가
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-semibold">이름 (식별용)</label>
<input type="text" class="form-control" name="name" placeholder="예: 알림용 봇" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Bot Token</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-key"></i></span>
<input type="text" class="form-control font-monospace" name="token"
placeholder="123456:ABC..." required>
</div>
<div class="form-text">BotFather에게 받은 API Token</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Chat ID</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-chat-dots"></i></span>
<input type="text" class="form-control font-monospace" name="chat_id"
placeholder="-100..." required>
</div>
<div class="form-text">메시지를 받을 채팅방 ID (그룹은 음수)</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">설명</label>
<textarea class="form-control" name="description" rows="2"
placeholder="선택 사항"></textarea>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">알림 유형</label>
<div class="d-flex gap-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="notify_types" value="auth"
id="add_auth" checked>
<label class="form-check-label" for="add_auth">
인증
</label>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
<button type="submit" class="btn btn-primary px-4">추가</button>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="notify_types" value="activity"
id="add_activity" checked>
<label class="form-check-label" for="add_activity">
활동
</label>
</div>
</form>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="notify_types" value="system"
id="add_system" checked>
<label class="form-check-label" for="add_system">
시스템
</label>
</div>
</div>
<small class="text-muted">선택한 알림 유형만 전송됩니다</small>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">설명</label>
<textarea class="form-control" name="description" rows="2" placeholder="선택 사항"></textarea>
</div>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
<button type="submit" class="btn btn-primary px-4">추가</button>
</div>
</form>
</div>
{% endblock %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -29,21 +29,30 @@
<!-- Skip to main content (접근성) -->
<a href="#main-content" class="visually-hidden-focusable">본문으로 건너뛰기</a>
{# 플래시 메시지 (전역) #}
{# 플래시 메시지 (좌측 상단 토스트 스타일) #}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="position-fixed end-0 p-3" style="z-index: 2000; top: 70px;">
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 2000; margin-top: 60px;">
{% for cat, msg in messages %}
<div class="alert alert-{{ cat }} alert-dismissible fade show shadow-lg" role="alert">
<i class="bi bi-{{ 'check-circle' if cat == 'success' else 'exclamation-triangle' }} me-2"></i>
{{ msg }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<div class="toast align-items-center text-white bg-{{ 'success' if cat == 'success' else 'danger' if cat == 'error' else 'primary' }} border-0 fade show"
role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="3000">
<div class="d-flex">
<div class="toast-body d-flex align-items-center">
<i
class="bi bi-{{ 'check-circle-fill' if cat == 'success' else 'exclamation-diamond-fill' }} me-2 fs-5"></i>
<div>{{ msg }}</div>
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"
aria-label="Close"></button>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
<div class="container-fluid">
@@ -110,6 +119,10 @@
<!-- Main Content -->
<main id="main-content"
class="{% if request.endpoint in ['auth.login', 'auth.register', 'auth.reset_password'] %}container mt-5{% else %}container mt-4 container-card{% endif %}">
{# 플래시 메시지 (컨텐츠 상단 표시) #}
{% block content %}{% endblock %}
</main>
@@ -133,6 +146,24 @@
{% endif %}
{% block scripts %}{% endblock %}
<!-- Auto-hide Toasts -->
<script>
document.addEventListener('DOMContentLoaded', function () {
var toastElList = [].slice.call(document.querySelectorAll('.toast'));
var toastList = toastElList.map(function (toastEl) {
// 부트스트랩 토스트 인스턴스 생성 (autohide: true 기본값)
var toast = new bootstrap.Toast(toastEl, { delay: 3000 });
toast.show();
// 3초 후 자동으로 DOM에서 제거하고 싶다면 이벤트 리스너 추가 가능
toastEl.addEventListener('hidden.bs.toast', function () {
// toastEl.remove(); // 필요시 제거
});
return toast;
});
});
</script>
</body>
</html>

View File

@@ -1,26 +1,170 @@
{% extends "base.html" %}
{% block title %}Edit XML File - Dell Server Info{% endblock %}
{% block title %}XML 편집: {{ filename }} - Dell Server Info{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/edit_xml.css') }}">
<style>
/* 전체 레이아웃 */
.editor-container {
display: flex;
flex-direction: column;
height: calc(100vh - 160px);
/* 헤더/푸터 제외 높이 (조정 가능) */
min-height: 600px;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 8px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
/* 툴바 (헤더) */
.editor-toolbar {
background-color: #f8fafc;
border-bottom: 1px solid #e2e8f0;
padding: 0.75rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.editor-title {
font-size: 1.1rem;
font-weight: 600;
color: #1e293b;
display: flex;
align-items: center;
gap: 0.5rem;
}
.editor-actions {
display: flex;
gap: 0.5rem;
}
/* 에디터 본문 */
#monaco-editor-root {
flex: 1;
width: 100%;
height: 100%;
}
/* 로딩 인디케이터 */
.editor-loading {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #64748b;
font-size: 1.2rem;
background: #f1f5f9;
}
</style>
{% endblock %}
{% block content %}
<div class="card shadow-lg">
<div class="card-header bg-primary text-white">
<h3>Edit XML File: <strong>{{ filename }}</strong></h3>
<div class="container-fluid py-4 h-100">
<!-- Breadcrumb / Navigation -->
<div class="mb-3 d-flex align-items-center">
<a href="{{ url_for('xml.xml_management') }}" class="text-decoration-none text-muted small fw-bold">
<i class="bi bi-arrow-left me-1"></i>목록으로 돌아가기
</a>
</div>
<div class="card-body">
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="form-group">
<label for="xmlContent">XML Content</label>
<textarea id="xmlContent" name="content" class="form-control" rows="20">{{ content }}</textarea>
<form id="editorForm" method="post" style="height: 100%;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<!-- Monaco Editor의 내용은 submit 시 이 textarea에 동기화됨 -->
<textarea name="content" id="hiddenContent" style="display:none;">{{ content }}</textarea>
<div class="editor-container">
<!-- Toolbar -->
<div class="editor-toolbar">
<div class="editor-title">
<i class="bi bi-filetype-xml text-primary fs-4"></i>
<span>{{ filename }}</span>
<span class="badge bg-light text-secondary border ms-2">XML</span>
</div>
<div class="editor-actions">
<!-- 포맷팅 버튼 (Monaco 기능 호출) -->
<button type="button" class="btn btn-white border text-dark btn-sm fw-bold" id="btnFormat">
<i class="bi bi-magic me-1 text-info"></i> 자동 정렬
</button>
<!-- 저장 버튼 -->
<button type="submit" class="btn btn-primary btn-sm fw-bold px-4">
<i class="bi bi-save me-1"></i> 저장하기
</button>
</div>
</div>
<button type="submit" class="btn btn-success mt-3">Save Changes</button>
<a href="{{ url_for('xml.xml_management') }}" class="btn btn-secondary mt-3">Cancel</a>
</form>
</div>
<!-- Editor Area -->
<div id="monaco-editor-root">
<div class="editor-loading">
<div class="spinner-border text-primary me-3" role="status"></div>
<div>에디터를 불러오는 중...</div>
</div>
</div>
</div>
</form>
</div>
{% endblock %}
{% block scripts %}
<!-- Monaco Editor Loader -->
<script src="https://unpkg.com/monaco-editor@0.45.0/min/vs/loader.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
if (typeof require === 'undefined') {
document.querySelector('.editor-loading').innerHTML =
'<div class="text-danger"><i class="bi bi-exclamation-triangle me-2"></i>Monaco Editor를 로드할 수 없습니다. 인터넷 연결을 확인하거나 CDN 차단을 확인하세요.</div>';
return;
}
require.config({ paths: { 'vs': 'https://unpkg.com/monaco-editor@0.45.0/min/vs' } });
require(['vs/editor/editor.main'], function () {
// 초기 컨텐츠 가져오기
var initialContent = document.getElementById('hiddenContent').value;
var container = document.getElementById('monaco-editor-root');
// 기존 로딩 메시지 제거
container.innerHTML = '';
// 에디터 생성
var editor = monaco.editor.create(container, {
value: initialContent,
language: 'xml',
theme: 'vs', // or 'vs-dark'
automaticLayout: true,
minimap: { enabled: true },
fontSize: 14,
scrollBeyondLastLine: false,
lineNumbers: 'on',
formatOnPaste: true,
formatOnType: true,
wordWrap: 'on'
});
// 1. 폼 제출 시 에디터 내용을 textarea에 동기화
document.getElementById('editorForm').addEventListener('submit', function () {
document.getElementById('hiddenContent').value = editor.getValue();
});
// 2. 자동 정렬(Format) 버튼 기능 연결
document.getElementById('btnFormat').addEventListener('click', function () {
editor.getAction('editor.action.formatDocument').run();
});
// 3. Ctrl+S 저장 단축키 지원
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, function () {
document.getElementById('editorForm').requestSubmit();
});
}, function (err) {
// 로드 실패 시 에러 표시
document.querySelector('.editor-loading').innerHTML =
'<div class="text-danger"><i class="bi bi-exclamation-triangle me-2"></i>에디터 리소스 로드 실패: ' + err.message + '</div>';
});
});
</script>
{% endblock %}

View File

@@ -21,30 +21,39 @@
{# IP 처리 카드 #}
<div class="col-lg-6">
<div class="card border shadow-sm h-100">
<div class="card-header bg-primary text-white border-0 py-2">
<h6 class="mb-0 fw-semibold">
<div class="card-header bg-light border-0 py-2">
<h6 class="mb-0">
<i class="bi bi-hdd-network me-2"></i>
IP 처리
</h6>
</div>
<div class="card-body p-4">
<form id="ipForm" method="post" action="{{ url_for('main.process_ips') }}">
<div class="card-body p-4 h-100 d-flex flex-column">
<form id="ipForm" method="post" action="{{ url_for('main.process_ips') }}" class="h-100 d-flex flex-column">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{# 스크립트 선택 #}
<div class="mb-3">
<label for="script" class="form-label">스크립트 선택</label>
<select id="script" name="script" class="form-select" required>
<select id="script" name="script" class="form-select" required autocomplete="off">
<option value="">스크립트를 선택하세요</option>
{% if grouped_scripts %}
{% for category, s_list in grouped_scripts.items() %}
<optgroup label="{{ category }}">
{% for script in s_list %}
<option value="{{ script }}">{{ script }}</option>
{% endfor %}
</optgroup>
{% endfor %}
{% else %}
{# 만약 grouped_scripts가 없는 경우(하위 호환) #}
{% for script in scripts %}
<option value="{{ script }}">{{ script }}</option>
{% endfor %}
{% endif %}
</select>
</div>
{# XML 파일 선택 (조건부) #}
<div class="mb-3" id="xmlFileGroup" style="display:none;">
<label for="xmlFile" class="form-label">XML 파일 선택</label>
<select id="xmlFile" name="xmlFile" class="form-select">
<option value="">XML 파일 선택</option>
{% for xml_file in xml_files %}
@@ -54,18 +63,36 @@
</div>
{# IP 주소 입력 #}
<div class="mb-3">
<label for="ips" class="form-label">
IP 주소 (각 줄에 하나)
<span class="badge bg-secondary ms-2" id="ipLineCount">0 대설정</span>
<div class="mb-3 flex-grow-1 d-flex flex-column">
<label for="ips" class="form-label w-100 d-flex justify-content-between align-items-end mb-2">
<span class="mb-1">
IP 주소
<span class="badge bg-secondary ms-1" id="ipLineCount">0</span>
</span>
<div class="d-flex align-items-center gap-1">
<button type="button" class="btn btn-sm btn-outline-secondary px-2 py-1" id="btnClearIps"
title="입력 내용 지우기" style="font-size: 0.75rem;">
<i class="bi bi-trash me-1"></i>지우기
</button>
<button type="button" class="btn btn-sm btn-outline-primary px-2 py-1" id="btnStartScan"
title="10.10.0.1 ~ 255 자동 스캔" style="font-size: 0.75rem;">
<i class="bi bi-search me-1"></i>IP 스캔
</button>
</div>
</label>
<textarea id="ips" name="ips" rows="4" class="form-control font-monospace"
placeholder="예:&#10;192.168.1.1&#10;192.168.1.2&#10;192.168.1.3" required></textarea>
<textarea id="ips" name="ips" class="form-control font-monospace flex-grow-1"
placeholder="예:&#10;192.168.1.1&#10;192.168.1.2" required style="resize: none;"></textarea>
</div>
<button type="submit" class="btn btn-primary w-100">
처리
</button>
<div class="mt-auto">
<button type="submit"
class="btn btn-white bg-white border shadow-sm w-100 py-2 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move">
<div class="rounded-circle bg-primary bg-opacity-10 text-primary p-1">
<i class="bi bi-play-circle-fill fs-5"></i>
</div>
<span class="fw-medium text-dark" style="font-size: 0.8rem;">처리 시작</span>
</button>
</div>
</form>
</div>
</div>
@@ -74,8 +101,8 @@
{# 공유 작업 카드 #}
<div class="col-lg-6">
<div class="card border shadow-sm h-100">
<div class="card-header bg-success text-white border-0 py-2">
<h6 class="mb-0 fw-semibold">
<div class="card-header bg-light border-0 py-2">
<h6 class="mb-0">
<i class="bi bi-share me-2"></i>
공유 작업
</h6>
@@ -93,16 +120,34 @@
style="font-size: 0.95rem;" placeholder="서버 리스트를 입력하세요..."></textarea>
</div>
<div class="d-flex gap-2">
<button type="submit" formaction="{{ url_for('utils.update_server_list') }}" class="btn btn-secondary">
MAC to Excel
</button>
<button type="submit" formaction="{{ url_for('utils.update_guid_list') }}" class="btn btn-success">
GUID to Excel
</button>
<button type="submit" formaction="{{ url_for('utils.update_gpu_list') }}" class="btn btn-warning">
GPU to Excel
</button>
<div class="row g-2">
<div class="col-4">
<button type="submit" formaction="{{ url_for('utils.update_server_list') }}"
class="btn btn-white bg-white border shadow-sm w-100 py-2 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move h-100">
<div class="rounded-circle bg-primary bg-opacity-10 text-primary p-1">
<i class="bi bi-file-earmark-spreadsheet fs-5"></i>
</div>
<span class="fw-medium text-dark" style="font-size: 0.8rem;">MAC to Excel</span>
</button>
</div>
<div class="col-4">
<button type="submit" formaction="{{ url_for('utils.update_guid_list') }}"
class="btn btn-white bg-white border shadow-sm w-100 py-2 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move h-100">
<div class="rounded-circle bg-success bg-opacity-10 text-success p-1">
<i class="bi bi-file-earmark-excel fs-5"></i>
</div>
<span class="fw-medium text-dark" style="font-size: 0.8rem;">GUID to Excel</span>
</button>
</div>
<div class="col-4">
<button type="submit" formaction="{{ url_for('utils.update_gpu_list') }}"
class="btn btn-white bg-white border shadow-sm w-100 py-2 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move h-100">
<div class="rounded-circle bg-danger bg-opacity-10 text-danger p-1">
<i class="bi bi-gpu-card fs-5"></i>
</div>
<span class="fw-medium text-dark" style="font-size: 0.8rem;">GPU to Excel</span>
</button>
</div>
</div>
</form>
</div>
@@ -142,59 +187,108 @@
</div>
<div class="card-body p-4 file-tools">
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xxl-5 g-3 align-items-end">
<div class="d-flex flex-column gap-3">
<!-- ZIP 다운로드 -->
<div class="col">
<label class="form-label text-nowrap">ZIP 다운로드</label>
<form method="post" action="{{ url_for('main.download_zip') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="input-group">
<input type="text" class="form-control" name="zip_filename" placeholder="파일명" required>
<button class="btn btn-primary" type="submit">다운로드</button>
<!-- 상단: 입력형 도구 (다운로드/백업) -->
<div class="row g-2">
<!-- ZIP 다운로드 -->
<div class="col-6">
<div class="card h-100 border-primary-subtle bg-primary-subtle bg-opacity-10">
<div class="card-body p-2 d-flex flex-column justify-content-center">
<h6 class="card-title fw-bold text-primary mb-1 small" style="font-size: 0.75rem;">
<i class="bi bi-file-earmark-zip me-1"></i>ZIP
</h6>
<form method="post" action="{{ url_for('main.download_zip') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="input-group input-group-sm">
<input type="text" class="form-control border-primary-subtle form-control-sm"
name="zip_filename" placeholder="파일명" required
style="font-size: 0.75rem; padding: 0.2rem 0.5rem;">
<button class="btn btn-primary btn-sm px-2" type="submit">
<i class="bi bi-download" style="font-size: 0.75rem;"></i>
</button>
</div>
</form>
</div>
</div>
</form>
</div>
</div>
<!-- 파일 백업 -->
<div class="col">
<label class="form-label text-nowrap">파일 백업</label>
<form method="post" action="{{ url_for('main.backup_files') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="input-group">
<input type="text" class="form-control" name="backup_prefix" placeholder="PO로 시작">
<button class="btn btn-success" type="submit">백업</button>
<!-- 파일 백업 -->
<div class="col-6">
<div class="card h-100 border-success-subtle bg-success-subtle bg-opacity-10">
<div class="card-body p-2 d-flex flex-column justify-content-center">
<h6 class="card-title fw-bold text-success mb-1 small" style="font-size: 0.75rem;">
<i class="bi bi-hdd-network me-1"></i>백업
</h6>
<form method="post" action="{{ url_for('main.backup_files') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="input-group input-group-sm">
<input type="text" class="form-control border-success-subtle form-control-sm"
name="backup_prefix" placeholder="ex)PO-20251117-0015_20251223_판교_R6615(TY1A)"
style="font-size: 0.75rem; padding: 0.2rem 0.5rem;">
<button class="btn btn-success btn-sm px-2" type="submit">
<i class="bi bi-save" style="font-size: 0.75rem;"></i>
</button>
</div>
</form>
</div>
</div>
</form>
</div>
</div>
<!-- MAC 파일 이동 -->
<div class="col">
<label class="form-label text-nowrap">MAC 파일 이동</label>
<form id="macMoveForm" method="post" action="{{ url_for('utils.move_mac_files') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-warning w-100" type="submit">MAC Move</button>
</form>
</div>
<!-- 하단: 원클릭 액션 (파일 정리) -->
<div class="card bg-light border-0">
<div class="card-body p-3">
<small class="text-muted fw-bold text-uppercase mb-2 d-block">
<i class="bi bi-folder-symlink me-1"></i>파일 정리 (Quick Move)
</small>
<div class="row g-2">
<!-- MAC Move -->
<div class="col-4">
<form id="macMoveForm" method="post" action="{{ url_for('utils.move_mac_files') }}" class="h-100">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button
class="btn btn-white bg-white border shadow-sm w-100 h-100 py-1 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move"
type="submit">
<div class="rounded-circle bg-primary bg-opacity-10 text-primary p-1">
<i class="bi bi-cpu fs-6"></i>
</div>
<span class="fw-medium text-dark" style="font-size: 0.75rem;">MAC</span>
</button>
</form>
</div>
<!-- GUID 파일 이동 -->
<div class="col">
<label class="form-label text-nowrap">GUID 파일 이동</label>
<form id="guidMoveForm" method="post" action="{{ url_for('utils.move_guid_files') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-info w-100" type="submit">GUID Move</button>
</form>
</div>
<!-- GUID Move -->
<div class="col-4">
<form id="guidMoveForm" method="post" action="{{ url_for('utils.move_guid_files') }}" class="h-100">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button
class="btn btn-white bg-white border shadow-sm w-100 h-100 py-1 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move"
type="submit">
<div class="rounded-circle bg-success bg-opacity-10 text-success p-1">
<i class="bi bi-fingerprint fs-6"></i>
</div>
<span class="fw-medium text-dark" style="font-size: 0.75rem;">GUID</span>
</button>
</form>
</div>
<!-- GPU 파일 이동 -->
<div class="col">
<label class="form-label text-nowrap">GPU 파일 이동</label>
<form id="gpuMoveForm" method="post" action="{{ url_for('utils.move_gpu_files') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-secondary w-100" type="submit">GPU Move</button>
</form>
<div class="col-4">
<form id="gpuMoveForm" method="post" action="{{ url_for('utils.move_gpu_files') }}" class="h-100">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button
class="btn btn-white bg-white border shadow-sm w-100 h-100 py-1 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move"
type="submit">
<div class="rounded-circle bg-danger bg-opacity-10 text-danger p-1">
<i class="bi bi-gpu-card fs-6"></i>
</div>
<span class="fw-medium text-dark" style="font-size: 0.75rem;">GPU</span>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -386,17 +480,76 @@
</div>
</div>
</div>
{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
<!-- Tom Select CSS (Bootstrap 5 theme) -->
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.bootstrap5.min.css" rel="stylesheet">
<style>
/* Tom Select 미세 조정 */
.ts-wrapper.form-select {
padding: 0 !important;
border: none !important;
}
.ts-control {
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 0.375rem 0.75rem;
}
.ts-wrapper.focus .ts-control {
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
/* Quick Move 버튼 호버 효과 */
.btn-quick-move {
transition: all 0.2s ease-in-out;
}
.btn-quick-move:hover {
transform: translateY(-3px);
box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .1) !important;
background-color: #f8f9fa !important;
border-color: #dee2e6 !important;
}
.btn-quick-move:active {
transform: translateY(0);
}
</style>
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="{{ url_for('static', filename='js/index.js') }}"></script>
<!-- Tom Select JS -->
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Tom Select 초기화
// 모바일 등 환경 고려, 검색 가능하게 설정
if (document.getElementById('script')) {
new TomSelect("#script", {
create: false,
sortField: {
field: "text",
direction: "asc"
},
placeholder: "스크립트를 검색하거나 선택하세요...",
plugins: ['clear_button'],
allowEmptyOption: true,
});
}
});
</script>
<script src="{{ url_for('static', filename='js/index.js') }}?v={{ range(1, 100000) | random }}"></script>
<!-- 외부 script.js 파일 (IP 폼 처리 로직 포함) -->
<script src="{{ url_for('static', filename='script.js') }}"></script>
<script src="{{ url_for('static', filename='script.js') }}?v={{ range(1, 100000) | random }}"></script>
{% endblock %}

View File

@@ -1,215 +1,454 @@
{% extends "base.html" %}
{% block title %}XML 파일 관리 & 배포 - Dell Server Info{% endblock %}
{% block title %}XML 설정 관리 & 배포 - Dell Server Info{% endblock %}
{% block extra_css %}
<!-- Existing SCP CSS for legacy support or specific components -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/scp.css') }}">
<!-- Overriding/New Styles for Modern Look -->
<style>
/* 드래그 앤 드롭 영역 스타일 */
.drop-zone {
border: 2px dashed #cbd5e1;
border-radius: 12px;
background-color: #f8fafc;
transition: all 0.2s ease;
text-align: center;
padding: 2rem;
cursor: pointer;
position: relative;
}
.drop-zone:hover,
.drop-zone.dragover {
border-color: #3b82f6;
background-color: #eff6ff;
}
.drop-zone-icon {
font-size: 2.5rem;
color: #64748b;
margin-bottom: 1rem;
}
.drop-zone-text {
font-weight: 500;
color: #334155;
margin-bottom: 0.5rem;
}
.drop-zone-hint {
font-size: 0.875rem;
color: #94a3b8;
}
.drop-zone input[type="file"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
/* 카드 그리드 스타일 (index.html과 유사) */
.xml-file-card {
background: white;
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 1rem;
transition: all 0.2s ease;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.xml-file-card:hover {
transform: translateY(-3px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
border-color: #3b82f6;
}
.file-icon-wrapper {
width: 48px;
height: 48px;
border-radius: 10px;
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
color: #2563eb;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
margin-bottom: 1rem;
}
.file-name {
font-weight: 600;
color: #1e293b;
margin-bottom: 0.5rem;
word-break: break-all;
line-height: 1.4;
}
.file-meta {
font-size: 0.75rem;
color: #64748b;
margin-bottom: 1rem;
}
.card-actions {
display: flex;
gap: 0.5rem;
margin-top: auto;
}
.btn-action {
flex: 1;
padding: 0.4rem;
font-size: 0.8rem;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
transition: all 0.15s;
}
.section-header {
display: flex;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e2e8f0;
}
.section-title {
font-size: 1.25rem;
font-weight: 700;
color: #0f172a;
margin: 0;
}
</style>
{% endblock %}
{% block content %}
<h1 class="main-title">설정 파일 관리 (SCP)</h1>
<p class="subtitle">iDRAC 서버 설정(XML)을 내보내거나 가져오고, 버전을 비교할 수 있습니다.</p>
<div class="container-fluid py-4">
<div class="row">
<!-- 왼쪽: 파일 업로드 및 내보내기 -->
<div class="col-md-4">
<div class="card">
<div class="card-header-custom">
<span><i class="fas fa-cloud-upload-alt me-2"></i>파일 등록</span>
</div>
<div class="card-body">
<!-- 1. PC에서 업로드 -->
<h6 class="mb-3"><i class="fas fa-laptop me-2"></i>PC에서 업로드</h6>
<form action="{{ url_for('xml.upload_xml') }}" method="POST" enctype="multipart/form-data" class="mb-4">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="upload-section">
<div class="mb-2">
<div class="custom-file">
<input type="file" class="custom-file-input" id="xmlFile" name="xmlFile" accept=".xml"
onchange="updateFileName(this)">
<label class="custom-file-label" for="xmlFile" id="fileLabel">파일 선택</label>
</div>
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-upload me-1"></i>업로드
</button>
</div>
</form>
<hr>
<!-- 2. iDRAC에서 내보내기 -->
<h6 class="mb-3"><i class="fas fa-server me-2"></i>iDRAC에서 추출 (Export)</h6>
<button type="button" class="btn btn-outline-primary w-100" data-bs-toggle="modal"
data-bs-target="#exportModal">
<i class="fas fa-download me-1"></i>설정 추출하기
</button>
</div>
<!-- Header Section -->
<div class="row mb-4">
<div class="col">
<h2 class="fw-bold mb-1">
<i class="bi bi-file-earmark-code text-primary me-2"></i>
설정 파일 관리
</h2>
<p class="text-muted mb-0">서버 설정(XML) 파일을 업로드, 관리 및 배포합니다.</p>
</div>
<div class="col-auto align-self-end">
<button class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#exportModal">
<i class="bi bi-server me-2"></i>iDRAC에서 추출
</button>
</div>
</div>
<!-- 오른쪽: 파일 목록 및 작업 -->
<div class="col-md-8">
<div class="card">
<div class="card-header-custom">
<span><i class="fas fa-list me-2"></i>파일 목록</span>
<button class="btn btn-light btn-sm text-primary" id="compareBtn"
data-url="{{ url_for('scp.diff_scp') }}" onclick="compareSelected()">
<i class="fas fa-exchange-alt me-1"></i>선택 비교
</button>
</div>
<div class="card-body">
{% if xml_files %}
<div class="file-list">
{% for xml_file in xml_files %}
<div class="icon-badge-item">
<div class="icon-badge-left">
<input type="checkbox" class="select-checkbox file-selector" value="{{ xml_file }}">
<div class="file-icon-small">
<i class="fas fa-file-code"></i>
<div class="row g-4">
<!-- Left: Upload Section (30% on large screens) -->
<div class="col-lg-4 col-xl-3">
<div class="card border shadow-sm h-100">
<div class="card-header bg-white border-bottom-0 pt-4 pb-0">
<h6 class="fw-bold mb-0 text-dark">
<i class="bi bi-cloud-upload me-2 text-primary"></i>파일 업로드
</h6>
</div>
<div class="card-body">
<form action="{{ url_for('xml.upload_xml') }}" method="POST" enctype="multipart/form-data"
id="uploadForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="drop-zone" id="dropZone">
<input type="file" name="xmlFile" id="xmlFile" accept=".xml"
onchange="handleFileSelect(this)">
<div class="drop-zone-icon">
<i class="bi bi-file-earmark-arrow-up"></i>
</div>
<div class="file-name-section">
<span class="file-name-badge" title="{{ xml_file }}">{{ xml_file }}</span>
<span class="badge-custom">XML</span>
<div class="drop-zone-text" id="dropZoneText">
클릭하여 파일 선택<br>또는 파일을 여기로 드래그
</div>
<div class="drop-zone-hint">XML 파일만 지원됩니다.</div>
</div>
<div class="action-buttons">
<!-- 배포 버튼 -->
<button type="button" class="btn btn-info btn-sm text-white"
onclick="openDeployModal('{{ xml_file }}')">
<i class="fas fa-plane-departure"></i> <span>배포</span>
</button>
<!-- 편집 버튼 -->
<a href="{{ url_for('xml.edit_xml', filename=xml_file) }}" class="btn btn-success btn-sm">
<i class="fas fa-edit"></i> <span>편집</span>
</a>
<!-- 삭제 버튼 -->
<form action="{{ url_for('xml.delete_xml', filename=xml_file) }}" method="POST"
style="display:inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button type="submit" class="btn btn-danger btn-sm"
onclick="return confirm('정말 삭제하시겠습니까?')">
<i class="fas fa-trash"></i> <span>삭제</span>
</button>
</form>
</div>
<button type="submit" class="btn btn-primary w-100 mt-3 shadow-sm">
<i class="bi bi-upload me-2"></i>업로드 시작
</button>
</form>
<div class="alert alert-light mt-4 border" role="alert">
<h6 class="alert-heading fs-6 fw-bold"><i class="bi bi-info-circle me-2"></i>도움말</h6>
<p class="mb-0 fs-small text-muted" style="font-size: 0.85rem;">
업로드된 XML 파일을 사용하여 여러 서버에 동일한 설정을 일괄 배포할 수 있습니다.
'비교' 기능을 사용하여 버전 간 차이를 확인하세요.
</p>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-message">
<i class="fas fa-folder-open" style="font-size: 2rem; color: #ddd;"></i>
<p class="mt-2 mb-0">파일이 없습니다.</p>
</div>
</div>
<!-- Right: File List (70%) -->
<div class="col-lg-8 col-xl-9">
<div class="card border shadow-sm h-100">
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<h6 class="fw-bold mb-0 text-dark me-3">
<i class="bi bi-list-check me-2 text-success"></i>파일 목록
</h6>
<span class="badge bg-light text-dark border">{{ xml_files|length }}개 파일</span>
</div>
<div>
<button class="btn btn-sm btn-outline-secondary" id="compareBtn"
data-url="{{ url_for('scp.diff_scp') }}" onclick="compareSelected()">
<i class="bi bi-arrow-left-right me-1"></i>선택 비교
</button>
</div>
</div>
<div class="card-body bg-light">
{% if xml_files %}
<!-- 카드 크기 조정: 한 줄에 4개(xxl), 3개(xl) 등으로 조금 더 키움 -->
<div class="row row-cols-1 row-cols-lg-2 row-cols-xl-3 row-cols-xxl-4 g-3">
{% for xml_file in xml_files %}
<div class="col">
<div class="xml-file-card position-relative p-3 h-100 d-flex flex-column">
<div class="position-absolute top-0 end-0 p-2 me-1">
<input type="checkbox" class="form-check-input file-selector border-secondary"
value="{{ xml_file }}" style="cursor: pointer;">
</div>
<div class="d-flex align-items-center mb-3">
<div class="file-icon-wrapper me-3 mb-0 shadow-sm"
style="width: 42px; height: 42px; font-size: 1.4rem;">
<i class="bi bi-filetype-xml"></i>
</div>
<div class="file-name text-truncate fw-bold mb-0 text-dark"
style="max-width: 140px; font-size: 0.95rem;" title="{{ xml_file }}">
{{ xml_file }}
</div>
</div>
<div class="mt-auto pt-3 border-top">
<div class="d-flex gap-2">
<!-- 배포 버튼 -->
<button type="button"
class="btn btn-sm btn-primary flex-fill d-flex align-items-center justify-content-center gap-1"
onclick="openDeployModal('{{ xml_file }}')" title="배포">
<i class="bi bi-send-fill"></i> <span class="small fw-bold">배포</span>
</button>
<!-- 편집 버튼 -->
<a href="{{ url_for('xml.edit_xml', filename=xml_file) }}"
class="btn btn-sm btn-white border flex-fill d-flex align-items-center justify-content-center gap-1 text-dark bg-white"
title="편집">
<i class="bi bi-pencil-fill text-secondary"></i> <span
class="small fw-bold">편집</span>
</a>
<!-- 삭제 버튼 -->
<form action="{{ url_for('xml.delete_xml', filename=xml_file) }}" method="POST"
class="d-flex flex-fill m-0" style="min-width: 0;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button type="submit"
class="btn btn-sm btn-white border w-100 d-flex align-items-center justify-content-center gap-1 text-danger bg-white"
onclick="return confirm('정말 삭제하시겠습니까?')" title="삭제">
<i class="bi bi-trash-fill"></i> <span class="small fw-bold">삭제</span>
</button>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5 my-5">
<div class="mb-3 text-secondary" style="font-size: 3rem; opacity: 0.3;">
<i class="bi bi-folder2-open"></i>
</div>
<h5 class="text-secondary fw-normal">등록된 파일이 없습니다.</h5>
<p class="text-muted">좌측 패널에서 XML 파일을 업로드해주세요.</p>
</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Export Modal -->
<!-- Export Modal (Include existing modal logic but restyled) -->
<div class="modal fade" id="exportModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg">
<form action="{{ url_for('scp.export_scp') }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="modal-header">
<h5 class="modal-title">iDRAC 설정 내보내기</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<div class="modal-header bg-primary text-white">
<h5 class="modal-title fs-6 fw-bold"><i class="bi bi-download me-2"></i>iDRAC 설정 내보내기</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-info py-2" style="font-size: 0.9rem;">
<i class="fas fa-info-circle me-1"></i> 네트워크 공유 폴더(CIFS)가 필요합니다.
<div class="modal-body p-4">
<div class="alert alert-info py-2 small mb-4">
<i class="bi bi-info-circle-fill me-2"></i> CIFS 네트워크 공유 폴더가 필요합니다.
</div>
<h6>대상 iDRAC</h6>
<div class="mb-2"><input type="text" class="form-control" name="target_ip" placeholder="iDRAC IP"
required></div>
<div class="row mb-3">
<div class="col"><input type="text" class="form-control" name="username" placeholder="User"
required></div>
<div class="col"><input type="password" class="form-control" name="password"
placeholder="Password" required></div>
<h6 class="text-primary fw-bold mb-3 small text-uppercase">대상 iDRAC</h6>
<div class="form-floating mb-2">
<input type="text" class="form-control" id="targetIp" name="target_ip" placeholder="IP"
required>
<label for="targetIp">iDRAC IP Address</label>
</div>
<div class="row g-2 mb-4">
<div class="col">
<div class="form-floating">
<input type="text" class="form-control" id="targetUser" name="username"
placeholder="User" required>
<label for="targetUser">Username</label>
</div>
</div>
<div class="col">
<div class="form-floating">
<input type="password" class="form-control" id="targetPwd" name="password"
placeholder="Pwd" required>
<label for="targetPwd">Password</label>
</div>
</div>
</div>
<hr>
<h6>네트워크 공유 (저장소)</h6>
<div class="mb-2"><input type="text" class="form-control" name="share_ip"
placeholder="Share Server IP" required></div>
<div class="mb-2"><input type="text" class="form-control" name="share_name"
placeholder="Share Name (e.g. public)" required></div>
<div class="mb-2"><input type="text" class="form-control" name="filename"
placeholder="Save Filename (e.g. backup.xml)" required></div>
<div class="row">
<div class="col"><input type="text" class="form-control" name="share_user"
placeholder="Share User"></div>
<div class="col"><input type="password" class="form-control" name="share_pwd"
placeholder="Share Password"></div>
<h6 class="text-success fw-bold mb-3 small text-uppercase">저장소 (CIFS Share)</h6>
<div class="row g-2 mb-2">
<div class="col-8">
<div class="form-floating">
<input type="text" class="form-control" name="share_ip" placeholder="IP" required>
<label>Share IP</label>
</div>
</div>
<div class="col-4">
<div class="form-floating">
<input type="text" class="form-control" name="share_name" placeholder="Name" required>
<label>Share Name</label>
</div>
</div>
</div>
<div class="form-floating mb-2">
<input type="text" class="form-control" name="filename" placeholder="Filename" required>
<label>저장할 파일명 (예: backup.xml)</label>
</div>
<div class="row g-2">
<div class="col">
<div class="form-floating">
<input type="text" class="form-control" name="share_user" placeholder="User">
<label>Share User</label>
</div>
</div>
<div class="col">
<div class="form-floating">
<input type="password" class="form-control" name="share_pwd" placeholder="Pwd">
<label>Share Password</label>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
<button type="submit" class="btn btn-primary">내보내기 시작</button>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">취소</button>
<button type="submit" class="btn btn-primary px-4">내보내기 실행</button>
</div>
</form>
</div>
</div>
</div>
<!-- Deploy (Import) Modal -->
<!-- Deploy Modal -->
<div class="modal fade" id="deployModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg">
<form action="{{ url_for('scp.import_scp') }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="modal-header">
<h5 class="modal-title">설정 배포 (Import)</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<div class="modal-header bg-danger text-white">
<h5 class="modal-title fs-6 fw-bold"><i class="bi bi-send-fill me-2"></i>설정 배포 (Import)</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning py-2" style="font-size: 0.9rem;">
<i class="fas fa-exclamation-triangle me-1"></i> 적용 후 서버가 재부팅될 수 있습니다.
<div class="modal-body p-4">
<div class="alert alert-warning py-2 small mb-4">
<i class="bi bi-exclamation-triangle-fill me-2"></i> 적용 후 서버가 재부팅될 수 있습니다.
</div>
<div class="mb-3">
<label class="form-label">배포 파일</label>
<input type="text" class="form-control" id="deployFilename" name="filename" readonly>
<div class="mb-4">
<label class="form-label fw-bold small text-muted">배포 파일</label>
<div class="input-group">
<span class="input-group-text bg-light"><i class="bi bi-file-code"></i></span>
<input type="text" class="form-control fw-bold text-primary" id="deployFilename"
name="filename" readonly>
</div>
</div>
<h6>대상 iDRAC</h6>
<div class="mb-2"><input type="text" class="form-control" name="target_ip" placeholder="iDRAC IP"
required></div>
<div class="row mb-3">
<div class="col"><input type="text" class="form-control" name="username" placeholder="User"
required></div>
<div class="col"><input type="password" class="form-control" name="password"
placeholder="Password" required></div>
<h6 class="text-primary fw-bold mb-3 small text-uppercase">대상 iDRAC</h6>
<div class="form-floating mb-2">
<input type="text" class="form-control" name="target_ip" placeholder="IP" required>
<label>iDRAC IP</label>
</div>
<div class="row g-2 mb-4">
<div class="col">
<div class="form-floating">
<input type="text" class="form-control" name="username" placeholder="User" required>
<label>Username</label>
</div>
</div>
<div class="col">
<div class="form-floating">
<input type="password" class="form-control" name="password" placeholder="Pwd" required>
<label>Password</label>
</div>
</div>
</div>
<hr>
<h6>네트워크 공유 (소스 위치)</h6>
<div class="mb-2"><input type="text" class="form-control" name="share_ip"
placeholder="Share Server IP" required></div>
<div class="mb-2"><input type="text" class="form-control" name="share_name" placeholder="Share Name"
required></div>
<div class="row mb-3">
<div class="col"><input type="text" class="form-control" name="share_user"
placeholder="Share User"></div>
<div class="col"><input type="password" class="form-control" name="share_pwd"
placeholder="Share Password"></div>
<h6 class="text-success fw-bold mb-3 small text-uppercase">소스 위치 (CIFS Share)</h6>
<div class="row g-2 mb-2">
<div class="col-8">
<div class="form-floating">
<input type="text" class="form-control" name="share_ip" placeholder="IP" required>
<label>Share IP</label>
</div>
</div>
<div class="col-4">
<div class="form-floating">
<input type="text" class="form-control" name="share_name" placeholder="Name" required>
<label>Share Name</label>
</div>
</div>
</div>
<div class="row g-2 mb-4">
<div class="col">
<div class="form-floating">
<input type="text" class="form-control" name="share_user" placeholder="User">
<label>Share User</label>
</div>
</div>
<div class="col">
<div class="form-floating">
<input type="password" class="form-control" name="share_pwd" placeholder="Pwd">
<label>Share Password</label>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">적용 모드</label>
<select class="form-select" name="import_mode">
<div class="form-floating">
<select class="form-select" name="import_mode" id="importMode">
<option value="Replace">전체 교체 (Replace)</option>
<option value="Append">변경분만 적용 (Append)</option>
</select>
<label for="importMode">적용 모드</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
<button type="submit" class="btn btn-danger">배포 시작</button>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">취소</button>
<button type="submit" class="btn btn-danger px-4">배포 시작</button>
</div>
</form>
</div>
@@ -220,4 +459,67 @@
{% block scripts %}
<script src="{{ url_for('static', filename='js/scp.js') }}"></script>
<script>
// 드래그 앤 드롭 파일 처리
function handleFileSelect(input) {
const fileName = input.files[0]?.name;
const dropZoneText = document.getElementById('dropZoneText');
if (fileName) {
dropZoneText.innerHTML = `<span class="text-primary fw-bold">${fileName}</span><br><span class="text-muted small">파일이 선택되었습니다.</span>`;
document.getElementById('dropZone').classList.add('border-primary', 'bg-light');
} else {
dropZoneText.innerHTML = '클릭하여 파일 선택<br>또는 파일을 여기로 드래그';
document.getElementById('dropZone').classList.remove('border-primary', 'bg-light');
}
}
// 드래그 효과
const dropZone = document.getElementById('dropZone');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, unhighlight, false);
});
function highlight(e) {
dropZone.classList.add('dragover');
}
function unhighlight(e) {
dropZone.classList.remove('dragover');
}
dropZone.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
const input = document.getElementById('xmlFile');
if (files.length > 0) {
input.files = files; // input에 파일 할당
handleFileSelect(input);
}
}
// 툴팁 초기화 및 자동 닫기
document.addEventListener('DOMContentLoaded', function () {
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
});
});
</script>
{% endblock %}
```

View File

@@ -1,58 +1,173 @@
{% extends "base.html" %}
{% block title %}설정 파일 비교 - Dell Server Info{% endblock %}
{% block title %}설정 파일 비교: {{ file1 }} vs {{ file2 }}{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/scp.css') }}">
<!-- Monaco Diff Editor Styles -->
<style>
.diff-page-container {
display: flex;
flex-direction: column;
height: calc(100vh - 140px);
min-height: 600px;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 8px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.diff-toolbar {
background-color: #f8fafc;
border-bottom: 1px solid #e2e8f0;
padding: 0.75rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.diff-files {
display: flex;
align-items: center;
gap: 1.5rem;
font-weight: 600;
color: #334155;
}
.file-badge {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0.75rem;
background: #fff;
border: 1px solid #cbd5e1;
border-radius: 6px;
font-size: 0.9rem;
}
.diff-arrow {
color: #94a3b8;
font-size: 1.2rem;
}
#monaco-diff-root {
flex: 1;
width: 100%;
height: 100%;
}
</style>
{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>설정 파일 비교</h2>
<a href="{{ url_for('xml.xml_management') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i> 목록으로
<div class="container-fluid py-4 h-100">
<!-- Breadcrumb -->
<div class="mb-3">
<a href="{{ url_for('xml.xml_management') }}" class="text-decoration-none text-muted small fw-bold">
<i class="bi bi-arrow-left me-1"></i>목록으로 돌아가기
</a>
</div>
<div class="card mb-4">
<div class="card-header bg-info text-white">
<i class="fas fa-exchange-alt me-2"></i>비교 대상
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-md-5">
<h5>{{ file1 }}</h5>
<div class="diff-page-container">
<!-- Toolbar -->
<div class="diff-toolbar">
<div class="diff-files">
<div class="file-badge text-danger border-danger-subtle bg-danger-subtle">
<i class="bi bi-file-earmark-minus"></i>
<span>{{ file1 }}</span> <!-- Original -->
</div>
<div class="col-md-2">
<i class="fas fa-arrow-right text-muted"></i>
</div>
<div class="col-md-5">
<h5>{{ file2 }}</h5>
<i class="bi bi-arrow-right diff-arrow"></i>
<div class="file-badge text-success border-success-subtle bg-success-subtle">
<i class="bi bi-file-earmark-plus"></i>
<span>{{ file2 }}</span> <!-- Modified -->
</div>
</div>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-secondary" onclick="toggleInlineDiff()" id="viewToggleBtn">
<i class="bi bi-layout-split me-1"></i>Inline View
</button>
</div>
</div>
<!-- Monaco Diff Editor -->
<div id="monaco-diff-root"></div>
</div>
<div class="card">
<div class="card-header">
<i class="fas fa-code me-2"></i>Diff 결과
</div>
<div class="card-body p-0">
<div class="diff-container">
{% for line in diff_content.splitlines() %}
{% if line.startswith('+++') or line.startswith('---') %}
<span class="diff-line diff-header">{{ line }}</span>
{% elif line.startswith('+') %}
<span class="diff-line diff-add">{{ line }}</span>
{% elif line.startswith('-') %}
<span class="diff-line diff-del">{{ line }}</span>
{% else %}
<span class="diff-line">{{ line }}</span>
{% endif %}
{% endfor %}
</div>
</div>
</div>
<!-- Raw Content Hidden Inputs (Jinja2 will escape HTML entities automatically) -->
<textarea id="hidden_content1" style="display:none;">{{ content1 }}</textarea>
<textarea id="hidden_content2" style="display:none;">{{ content2 }}</textarea>
</div>
{% endblock %}
{% block scripts %}
<script src="https://unpkg.com/monaco-editor@0.45.0/min/vs/loader.js"></script>
<script>
let diffEditor = null;
let isInline = false;
document.addEventListener('DOMContentLoaded', function () {
// 1. Check for loader failure
if (typeof require === 'undefined') {
document.getElementById('monaco-diff-root').innerHTML =
'<div class="d-flex justify-content-center align-items-center h-100 text-danger">' +
'<i class="bi bi-exclamation-triangle me-2"></i>Monaco Editor 리소스를 불러올 수 없습니다. 인터넷 연결을 확인하세요.' +
'</div>';
return;
}
require.config({ paths: { 'vs': 'https://unpkg.com/monaco-editor@0.45.0/min/vs' } });
require(['vs/editor/editor.main'], function () {
// 2. Read content from hidden textareas
// Jinja2가 HTML escaping을 처리하므로, .value를 통해 원본 XML을 얻을 수 있습니다.
const content1 = document.getElementById('hidden_content1').value;
const content2 = document.getElementById('hidden_content2').value;
const originalModel = monaco.editor.createModel(content1, 'xml');
const modifiedModel = monaco.editor.createModel(content2, 'xml');
const container = document.getElementById('monaco-diff-root');
// 3. Create Diff Editor
diffEditor = monaco.editor.createDiffEditor(container, {
theme: 'vs',
originalEditable: false,
readOnly: true,
renderSideBySide: true, // Default: Split View
automaticLayout: true,
minimap: { enabled: true },
diffWordWrap: 'off'
});
diffEditor.setModel({
original: originalModel,
modified: modifiedModel
});
// 네비게이션 기능 추가
diffEditor.getNavigator();
}, function (err) {
document.getElementById('monaco-diff-root').innerHTML =
'<div class="d-flex justify-content-center align-items-center h-100 text-danger">' +
'<i class="bi bi-exclamation-triangle me-2"></i>에디터 로드 실패: ' + err.message + '</div>';
});
});
function toggleInlineDiff() {
if (!diffEditor) return;
isInline = !isInline;
diffEditor.updateOptions({
renderSideBySide: !isInline
});
const btn = document.getElementById('viewToggleBtn');
if (isInline) {
btn.innerHTML = '<i class="bi bi-layout-sidebar me-1"></i>Split View';
} else {
btn.innerHTML = '<i class="bi bi-layout-split me-1"></i>Inline View';
}
}
</script>
{% endblock %}

View File

@@ -47,6 +47,15 @@ class Config:
# ── DB (환경변수 DATABASE_URL 있으면 그 값을 우선 사용)
sqlite_path = (INSTANCE_DIR / "site.db").as_posix()
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", f"sqlite:///{sqlite_path}")
# DB 연결 안정성 옵션 (SQLite 락/쓰레드 문제 완화)
SQLALCHEMY_ENGINE_OPTIONS = {
"pool_pre_ping": True,
"pool_recycle": 280,
}
if SQLALCHEMY_DATABASE_URI.startswith("sqlite"):
SQLALCHEMY_ENGINE_OPTIONS["connect_args"] = {"check_same_thread": False}
SQLALCHEMY_TRACK_MODIFICATIONS = False
# ── Telegram (미설정 시 기능 비활성처럼 동작)
@@ -78,6 +87,7 @@ class Config:
# ── 세션
PERMANENT_SESSION_LIFETIME = timedelta(minutes=int(os.getenv("SESSION_MINUTES", 30)))
SESSION_PERMANENT = True # 브라우저 닫아도 세션 유지 (타임아웃까지)
# ── SocketIO
# threading / eventlet / gevent 중 선택. 기본은 threading (Windows 안정)

756
data/logs/2025-11-29.log Normal file
View File

@@ -0,0 +1,756 @@
2025-11-29 08:14:35,726 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-29 08:14:35,756 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-29 08:14:35,756 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-29 08:14:35,783 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-29 08:14:35,803 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-29 08:14:35,803 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-29 08:14:35,803 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-29 08:14:35,803 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-29 08:14:35,856 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-29 08:14:35,856 [INFO] werkzeug: Press CTRL+C to quit
2025-11-29 08:14:36,951 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getMe "HTTP/1.1 200 OK"
2025-11-29 08:14:37,184 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/deleteWebhook "HTTP/1.1 200 OK"
2025-11-29 08:14:37,185 [INFO] telegram.ext.Application: Application started
2025-11-29 08:14:38,056 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:38] "GET / HTTP/1.1" 302 -
2025-11-29 08:14:38,088 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:38] "GET /login?next=/ HTTP/1.1" 200 -
2025-11-29 08:14:38,346 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:38] "GET /static/style.css HTTP/1.1" 200 -
2025-11-29 08:14:38,694 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:38] "GET /static/favicon.ico HTTP/1.1" 200 -
2025-11-29 08:14:43,883 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-29 08:14:43,883 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-29 08:14:43,954 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=True
2025-11-29 08:14:43,954 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=True
2025-11-29 08:14:43,955 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-29 08:14:43,955 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-29 08:14:43,956 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:43] "POST /login HTTP/1.1" 302 -
2025-11-29 08:14:44,191 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:44] "GET /index HTTP/1.1" 200 -
2025-11-29 08:14:44,517 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:44] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 08:14:44,522 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:44] "GET /static/css/index.css HTTP/1.1" 200 -
2025-11-29 08:14:44,525 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:44] "GET /static/script.js HTTP/1.1" 200 -
2025-11-29 08:14:44,529 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:44] "GET /static/js/index.js HTTP/1.1" 200 -
2025-11-29 08:14:45,195 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-29 08:14:47,867 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:14:49,161 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:49] "GET /idrac HTTP/1.1" 308 -
2025-11-29 08:14:49,475 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:49] "GET /idrac/ HTTP/1.1" 200 -
2025-11-29 08:14:49,797 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:49] "GET /static/css/idrac_style.css HTTP/1.1" 200 -
2025-11-29 08:14:49,798 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:49] "GET /static/js/idrac_main.js HTTP/1.1" 200 -
2025-11-29 08:14:50,054 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:50] "GET /socket.io/?EIO=4&transport=polling&t=PhClt5V HTTP/1.1" 200 -
2025-11-29 08:14:50,136 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:50] "POST /socket.io/?EIO=4&transport=polling&t=PhClt98&sid=ybpAZIBf9CdxVyJIAAAA HTTP/1.1" 200 -
2025-11-29 08:14:50,139 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:50] "GET /idrac/api/groups HTTP/1.1" 200 -
2025-11-29 08:14:50,140 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:50] "GET /favicon.ico HTTP/1.1" 404 -
2025-11-29 08:14:50,142 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:50] "GET /idrac/api/servers HTTP/1.1" 200 -
2025-11-29 08:14:50,367 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:50] "GET /socket.io/?EIO=4&transport=polling&t=PhClt99&sid=ybpAZIBf9CdxVyJIAAAA HTTP/1.1" 200 -
2025-11-29 08:14:50,372 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:50] "GET /socket.io/?EIO=4&transport=polling&t=PhCltE3&sid=ybpAZIBf9CdxVyJIAAAA HTTP/1.1" 200 -
2025-11-29 08:14:55,662 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:14:55] "GET /idrac/api/firmware-versions HTTP/1.1" 200 -
2025-11-29 08:14:58,093 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:15:06,515 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:15:06] "POST /catalog/sync HTTP/1.1" 200 -
2025-11-29 08:15:08,327 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:15:13,235 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:15:13] "POST /catalog/sync HTTP/1.1" 200 -
2025-11-29 08:15:18,557 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:15:20,040 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:15:20] "GET /idrac/api/firmware-versions HTTP/1.1" 200 -
2025-11-29 08:15:28,783 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:15:28,937 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:15:28] "POST /catalog/sync HTTP/1.1" 200 -
2025-11-29 08:15:34,670 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:15:34] "POST /catalog/sync HTTP/1.1" 200 -
2025-11-29 08:15:39,011 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:15:49,239 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:15:59,473 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:16:09,699 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:16:19,927 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:16:30,154 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:16:40,381 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:16:50,615 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:17:00,843 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:17:02,435 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:17:02] "GET /socket.io/?EIO=4&transport=websocket&sid=ybpAZIBf9CdxVyJIAAAA HTTP/1.1" 200 -
2025-11-29 08:17:02,757 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:17:02] "GET /index HTTP/1.1" 200 -
2025-11-29 08:17:02,776 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:17:02] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 08:17:03,002 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:17:03] "GET /static/css/index.css HTTP/1.1" 304 -
2025-11-29 08:17:03,082 [INFO] app: LOGIN: already auth → /index
2025-11-29 08:17:03,082 [INFO] app: LOGIN: already auth → /index
2025-11-29 08:17:03,082 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:17:03] "GET /login?next=/ HTTP/1.1" 302 -
2025-11-29 08:17:03,083 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:17:03] "GET /static/js/index.js HTTP/1.1" 304 -
2025-11-29 08:17:03,191 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:17:03] "GET /index HTTP/1.1" 200 -
2025-11-29 08:17:03,390 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:17:03] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 08:17:03,515 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:17:03] "GET /static/js/index.js HTTP/1.1" 304 -
2025-11-29 08:17:03,516 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:17:03] "GET /static/script.js HTTP/1.1" 200 -
2025-11-29 08:17:03,517 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:17:03] "GET /static/css/index.css HTTP/1.1" 304 -
2025-11-29 08:17:11,069 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:17:21,296 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:17:31,524 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:17:41,752 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:17:51,985 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:18:02,214 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:18:12,440 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:18:22,665 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:18:32,897 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:18:43,122 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:18:53,349 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:19:03,575 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:19:13,803 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:19:24,030 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:19:34,258 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:19:44,487 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:19:54,717 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:19:58,781 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:19:58] "GET /idrac HTTP/1.1" 308 -
2025-11-29 08:19:58,783 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:19:58] "GET /idrac/ HTTP/1.1" 200 -
2025-11-29 08:19:59,032 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:19:59] "GET /static/css/idrac_style.css HTTP/1.1" 304 -
2025-11-29 08:19:59,107 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:19:59] "GET /static/js/idrac_main.js HTTP/1.1" 304 -
2025-11-29 08:19:59,421 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:19:59] "GET /idrac/api/groups HTTP/1.1" 200 -
2025-11-29 08:19:59,421 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:19:59] "GET /socket.io/?EIO=4&transport=polling&t=PhCn2c6 HTTP/1.1" 200 -
2025-11-29 08:19:59,422 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:19:59] "GET /idrac/api/servers HTTP/1.1" 200 -
2025-11-29 08:19:59,683 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:19:59] "POST /socket.io/?EIO=4&transport=polling&t=PhCn2h1&sid=2pMufQvxsqmN2V1_AAAC HTTP/1.1" 200 -
2025-11-29 08:19:59,728 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:19:59] "GET /socket.io/?EIO=4&transport=polling&t=PhCn2h2&sid=2pMufQvxsqmN2V1_AAAC HTTP/1.1" 200 -
2025-11-29 08:19:59,731 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:19:59] "GET /socket.io/?EIO=4&transport=polling&t=PhCn2lp&sid=2pMufQvxsqmN2V1_AAAC HTTP/1.1" 200 -
2025-11-29 08:20:00,769 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:20:00] "GET /idrac/api/firmware-versions HTTP/1.1" 200 -
2025-11-29 08:20:04,945 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:20:15,177 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:20:25,403 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:20:35,632 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:20:45,864 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:20:56,090 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:21:06,318 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:21:16,543 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:21:26,770 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:21:36,999 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:21:47,226 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:21:57,452 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:22:07,684 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:22:17,910 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:22:28,138 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:22:38,367 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:22:48,595 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:22:58,821 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:23:09,047 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:23:19,273 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:23:29,500 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:23:39,735 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:23:49,961 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:24:00,192 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:24:10,417 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:24:20,646 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:24:30,873 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:24:41,103 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:24:51,330 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:25:01,559 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:25:11,790 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:25:22,018 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:25:32,246 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:25:42,473 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:25:52,705 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:26:02,931 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:26:13,187 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:26:23,415 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:26:33,648 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:26:43,874 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:26:54,100 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:27:04,325 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:27:14,551 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:27:24,777 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:27:35,003 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:27:45,231 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:27:55,457 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:28:05,685 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:28:15,911 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:28:26,138 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:28:36,363 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:28:46,594 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:28:56,820 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:29:07,047 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:29:17,276 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:29:27,503 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:29:37,731 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:29:47,957 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:29:58,191 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:30:02,765 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 08:30:02] "GET /idrac/api/firmware-versions HTTP/1.1" 200 -
2025-11-29 08:30:08,423 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:30:18,651 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:30:28,878 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:30:39,106 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:30:49,332 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:30:59,559 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:31:09,787 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:31:20,014 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:31:30,240 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:31:40,465 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:31:51,174 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:32:01,407 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:32:11,642 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:32:21,877 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:32:32,111 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:32:42,345 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:32:52,577 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:33:02,812 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:33:13,045 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:33:23,280 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:33:33,516 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:33:43,749 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:33:53,983 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:34:04,216 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:34:14,449 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:34:24,682 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:34:34,916 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:34:45,150 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:34:55,385 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:35:05,621 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:35:15,853 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:35:26,087 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:35:36,321 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:35:46,554 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:35:56,788 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:36:07,023 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:36:17,255 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:36:27,488 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:36:37,721 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:36:47,957 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:36:58,191 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:37:08,430 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:37:18,665 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:37:28,899 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:37:39,149 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:37:49,392 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:37:59,628 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:38:09,861 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:38:20,093 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:38:30,329 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:38:40,563 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:38:50,800 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:39:01,033 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:39:11,270 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:39:21,504 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:39:31,738 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:39:41,972 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:39:52,206 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:40:02,440 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:40:12,673 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:40:22,908 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:40:33,144 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:40:43,379 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:40:53,613 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:41:03,847 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:41:14,080 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:41:24,344 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:41:34,578 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:41:44,811 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:41:55,043 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:42:05,277 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:42:15,512 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:42:25,750 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:42:35,985 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:42:46,218 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:42:56,453 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:43:06,688 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:43:16,922 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:43:27,155 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:43:37,387 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:43:47,621 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:43:57,855 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:44:08,089 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:44:18,322 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:44:28,558 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:44:38,791 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:44:49,028 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:44:59,263 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:45:09,502 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:45:19,736 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:45:29,970 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:45:40,205 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:45:50,438 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:46:00,670 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:46:10,903 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:46:21,137 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:46:31,370 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:46:41,623 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:46:51,857 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:47:02,090 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:47:12,331 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:47:22,565 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:47:32,798 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:47:43,033 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:47:53,269 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:48:03,502 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:48:13,738 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:48:23,972 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:48:34,207 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:48:44,441 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:48:55,138 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:49:05,370 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:49:15,600 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:49:25,831 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:49:36,063 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:49:46,293 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:49:56,524 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:50:06,756 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:50:16,990 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:50:27,219 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:50:37,450 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:50:47,679 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:50:57,912 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:51:08,143 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:51:18,374 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:51:28,606 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:51:38,837 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:51:49,067 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:51:59,297 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:52:09,529 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:52:19,759 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:52:29,991 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:52:40,221 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:52:50,451 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:53:00,680 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:53:10,913 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:53:21,144 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:53:31,376 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:53:41,606 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:53:51,836 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:54:02,066 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:54:12,297 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:54:22,528 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:54:32,759 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:54:42,991 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:54:53,222 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:55:03,452 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:55:13,683 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:55:23,914 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:55:34,145 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:55:44,377 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:55:54,607 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:56:04,839 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:56:15,070 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:56:25,301 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:56:35,532 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:56:45,763 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:56:55,995 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:57:06,224 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:57:16,456 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:57:26,708 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:57:36,938 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:57:47,169 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:57:57,400 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:58:07,631 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:58:17,861 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:58:28,093 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:58:38,324 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:58:48,555 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:58:58,785 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:59:09,016 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:59:19,246 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:59:29,478 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:59:39,709 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 08:59:49,939 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:00:00,169 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:00:10,400 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:00:20,630 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:00:30,863 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:00:41,094 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:00:51,324 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:01:01,554 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:01:11,785 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:01:22,016 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:01:32,246 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:01:42,477 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:01:52,708 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:02:02,938 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:02:13,169 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:02:23,400 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:02:33,653 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:02:43,885 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:02:54,115 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:03:04,348 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:03:14,583 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:03:24,813 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:03:35,043 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:03:45,273 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:03:55,505 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:04:05,736 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:04:15,969 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:04:26,200 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:04:36,429 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:04:46,659 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:04:56,899 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:05:07,130 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:05:17,362 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:05:27,593 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:05:37,824 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:05:48,055 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:05:58,746 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:06:08,975 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:06:19,203 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:06:29,433 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:06:39,662 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:06:49,891 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:07:00,120 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:07:10,348 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:07:20,577 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:07:30,807 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:07:41,036 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:07:51,266 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:08:01,495 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:08:11,725 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:08:21,954 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:08:32,184 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:08:42,414 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:08:52,644 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:09:02,873 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:09:13,102 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:09:23,339 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:09:33,567 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:09:43,796 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:09:54,025 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:10:04,255 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:10:14,485 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:10:24,714 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:10:34,948 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:10:45,179 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:10:55,409 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:11:05,638 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:11:15,867 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:11:26,097 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:11:36,327 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:11:46,557 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:11:56,788 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:12:07,017 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:12:17,247 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:12:27,477 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:12:37,706 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:12:47,935 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:12:58,165 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:13:08,394 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:13:18,624 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:13:28,853 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:13:39,082 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:13:49,315 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:13:59,543 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:14:09,772 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:14:20,002 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:14:30,230 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:14:40,460 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:14:50,689 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:15:00,918 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:15:11,147 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:15:21,376 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:15:31,605 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:15:41,834 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:15:52,063 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:16:02,294 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:16:12,523 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:16:22,753 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:16:32,982 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:16:43,212 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:16:53,442 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:17:03,671 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:17:13,901 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:17:24,129 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:17:34,359 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:17:44,587 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:17:54,818 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:18:05,047 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:18:15,276 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:18:25,505 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:18:35,735 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:18:45,964 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:18:56,193 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:19:06,422 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:19:16,652 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:19:26,880 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:19:37,109 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:19:47,339 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:19:57,568 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:20:07,798 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:20:18,028 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 09:20:21,965 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 09:20:21] "GET /idrac/api/firmware-versions HTTP/1.1" 200 -
2025-11-29 09:20:22,683 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 09:20:22] "GET /idrac/api/firmware-versions HTTP/1.1" 200 -
2025-11-29 09:20:24,123 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 09:20:24] "GET /idrac/api/firmware-versions HTTP/1.1" 200 -
2025-11-29 09:20:24,715 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 09:20:24] "GET /idrac/api/firmware-versions HTTP/1.1" 200 -
2025-11-29 09:20:28,257 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 11:25:00,388 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-29 11:25:00,411 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-29 11:25:00,411 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-29 11:25:00,511 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-29 11:25:00,524 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-29 11:25:00,525 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-29 11:25:00,524 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-29 11:25:00,525 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-29 11:25:00,608 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-29 11:25:00,609 [INFO] werkzeug: Press CTRL+C to quit
2025-11-29 11:25:01,945 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getMe "HTTP/1.1 200 OK"
2025-11-29 11:25:02,176 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/deleteWebhook "HTTP/1.1 200 OK"
2025-11-29 11:25:02,178 [INFO] telegram.ext.Application: Application started
2025-11-29 11:25:05,399 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:25:05] "GET / HTTP/1.1" 302 -
2025-11-29 11:25:05,453 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:25:05] "GET /login?next=/ HTTP/1.1" 200 -
2025-11-29 11:25:05,487 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:25:05] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 11:25:12,748 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:25:12] "POST /login HTTP/1.1" 500 -
2025-11-29 11:25:12,773 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:25:12] "GET /login?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 200 -
2025-11-29 11:25:12,779 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:25:12] "GET /login?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 200 -
2025-11-29 11:25:12,803 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:25:12] "GET /login?__debugger__=yes&cmd=resource&f=console.png&s=xoqKrRi6YNbxcjDr00Te HTTP/1.1" 200 -
2025-11-29 11:25:12,818 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:25:12] "GET /login?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 200 -
2025-11-29 11:25:12,859 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 11:25:23,086 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 11:25:33,316 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 11:25:46,857 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-29 11:25:46,875 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-29 11:25:46,875 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-29 11:25:46,891 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-29 11:25:46,904 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-29 11:25:46,904 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-29 11:25:46,904 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-29 11:25:46,904 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-29 11:25:46,917 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-29 11:25:46,917 [INFO] werkzeug: Press CTRL+C to quit
2025-11-29 11:25:47,784 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:25:47] "GET /login?next=/ HTTP/1.1" 200 -
2025-11-29 11:25:47,820 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:25:47] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 11:25:47,834 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:25:47] "GET /static/favicon.ico HTTP/1.1" 200 -
2025-11-29 11:25:47,981 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getMe "HTTP/1.1 200 OK"
2025-11-29 11:25:48,215 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/deleteWebhook "HTTP/1.1 200 OK"
2025-11-29 11:25:48,217 [INFO] telegram.ext.Application: Application started
2025-11-29 11:25:55,161 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-29 11:25:55,161 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-29 11:25:55,164 [WARNING] root: password verify failed: argon2: no backends available -- recommend you install one (e.g. 'pip install argon2_cffi')
2025-11-29 11:25:55,164 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=False
2025-11-29 11:25:55,164 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=False
2025-11-29 11:25:55,165 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:25:55] "POST /login HTTP/1.1" 200 -
2025-11-29 11:25:55,178 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:25:55] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 11:25:58,922 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 11:26:00,658 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-29 11:26:00,658 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-29 11:26:00,660 [WARNING] root: password verify failed: argon2: no backends available -- recommend you install one (e.g. 'pip install argon2_cffi')
2025-11-29 11:26:00,660 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=False
2025-11-29 11:26:00,660 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=False
2025-11-29 11:26:00,661 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:00] "POST /login HTTP/1.1" 200 -
2025-11-29 11:26:00,680 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:00] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 11:26:09,154 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 11:26:14,556 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-29 11:26:14,556 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-29 11:26:14,557 [WARNING] root: password verify failed: argon2: no backends available -- recommend you install one (e.g. 'pip install argon2_cffi')
2025-11-29 11:26:14,557 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=False
2025-11-29 11:26:14,557 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=False
2025-11-29 11:26:14,558 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:14] "POST /login HTTP/1.1" 200 -
2025-11-29 11:26:14,571 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:14] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 11:26:14,592 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:14] "GET /static/favicon.ico HTTP/1.1" 304 -
2025-11-29 11:26:16,303 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-29 11:26:16,303 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-29 11:26:16,305 [WARNING] root: password verify failed: argon2: no backends available -- recommend you install one (e.g. 'pip install argon2_cffi')
2025-11-29 11:26:16,305 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=False
2025-11-29 11:26:16,305 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=False
2025-11-29 11:26:16,306 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:16] "POST /login HTTP/1.1" 200 -
2025-11-29 11:26:16,318 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:16] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 11:26:16,338 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:16] "GET /static/favicon.ico HTTP/1.1" 304 -
2025-11-29 11:26:19,385 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 11:26:26,537 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-29 11:26:26,537 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-29 11:26:26,538 [WARNING] root: password verify failed: argon2: no backends available -- recommend you install one (e.g. 'pip install argon2_cffi')
2025-11-29 11:26:26,539 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=False
2025-11-29 11:26:26,539 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=False
2025-11-29 11:26:26,539 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:26] "POST /login HTTP/1.1" 200 -
2025-11-29 11:26:26,556 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:26] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 11:26:29,618 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 11:26:30,145 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-29 11:26:30,145 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-29 11:26:30,147 [WARNING] root: password verify failed: argon2: no backends available -- recommend you install one (e.g. 'pip install argon2_cffi')
2025-11-29 11:26:30,147 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=False
2025-11-29 11:26:30,147 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=False
2025-11-29 11:26:30,148 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:30] "POST /login HTTP/1.1" 200 -
2025-11-29 11:26:30,163 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:30] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 11:26:39,849 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 11:26:46,795 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-29 11:26:46,814 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-29 11:26:46,814 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-29 11:26:46,830 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-29 11:26:46,843 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-29 11:26:46,843 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-29 11:26:46,843 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-29 11:26:46,843 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-29 11:26:46,856 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-29 11:26:46,856 [INFO] werkzeug: Press CTRL+C to quit
2025-11-29 11:26:47,893 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getMe "HTTP/1.1 200 OK"
2025-11-29 11:26:48,119 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/deleteWebhook "HTTP/1.1 200 OK"
2025-11-29 11:26:48,121 [INFO] telegram.ext.Application: Application started
2025-11-29 11:26:48,563 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-29 11:26:48,563 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-29 11:26:48,614 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=False
2025-11-29 11:26:48,614 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=False
2025-11-29 11:26:48,623 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:48] "POST /login HTTP/1.1" 200 -
2025-11-29 11:26:48,656 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:48] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 11:26:48,664 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:48] "GET /static/favicon.ico HTTP/1.1" 304 -
2025-11-29 11:26:52,625 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-29 11:26:52,625 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-29 11:26:52,672 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=True
2025-11-29 11:26:52,672 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=True
2025-11-29 11:26:52,673 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-29 11:26:52,673 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-29 11:26:52,674 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:52] "POST /login HTTP/1.1" 302 -
2025-11-29 11:26:52,700 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:52] "GET /index HTTP/1.1" 200 -
2025-11-29 11:26:52,716 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:52] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 11:26:52,728 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:52] "GET /static/css/index.css HTTP/1.1" 200 -
2025-11-29 11:26:52,731 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:52] "GET /static/script.js HTTP/1.1" 304 -
2025-11-29 11:26:52,734 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:52] "GET /static/js/index.js HTTP/1.1" 200 -
2025-11-29 11:26:54,110 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-29 11:26:58,026 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:58] "GET /admin HTTP/1.1" 200 -
2025-11-29 11:26:58,039 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:58] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 11:26:58,057 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:58] "GET /static/js/admin.js HTTP/1.1" 200 -
2025-11-29 11:26:58,801 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 11:26:59,260 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:59] "GET /admin/settings HTTP/1.1" 500 -
2025-11-29 11:26:59,271 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:59] "GET /admin/settings?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 200 -
2025-11-29 11:26:59,274 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:59] "GET /admin/settings?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 200 -
2025-11-29 11:26:59,279 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:59] "GET /admin/settings?__debugger__=yes&cmd=resource&f=console.png&s=THSpWanxyHDEaEXGoJp8 HTTP/1.1" 200 -
2025-11-29 11:26:59,302 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:26:59] "GET /admin/settings?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 200 -
2025-11-29 11:27:09,027 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 11:27:19,254 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 11:27:29,482 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 11:27:34,883 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:27:34] "GET /admin/settings HTTP/1.1" 500 -
2025-11-29 11:27:34,895 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:27:34] "GET /admin/settings?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 304 -
2025-11-29 11:27:34,898 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:27:34] "GET /admin/settings?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 304 -
2025-11-29 11:27:34,904 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:27:34] "GET /admin/settings?__debugger__=yes&cmd=resource&f=console.png&s=THSpWanxyHDEaEXGoJp8 HTTP/1.1" 304 -
2025-11-29 11:27:39,464 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:27:39] "GET /idrac HTTP/1.1" 308 -
2025-11-29 11:27:39,485 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:27:39] "GET /idrac/ HTTP/1.1" 200 -
2025-11-29 11:27:39,505 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:27:39] "GET /static/css/idrac_style.css HTTP/1.1" 200 -
2025-11-29 11:27:39,524 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:27:39] "GET /static/js/idrac_main.js HTTP/1.1" 200 -
2025-11-29 11:27:39,552 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:27:39] "GET /socket.io/?EIO=4&transport=polling&t=PhDR_kS HTTP/1.1" 200 -
2025-11-29 11:27:39,555 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:27:39] "GET /idrac/api/servers HTTP/1.1" 200 -
2025-11-29 11:27:39,558 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:27:39] "GET /idrac/api/groups HTTP/1.1" 200 -
2025-11-29 11:27:39,559 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:27:39] "GET /favicon.ico HTTP/1.1" 404 -
2025-11-29 11:27:39,561 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:27:39] "POST /socket.io/?EIO=4&transport=polling&t=PhDR_kc&sid=Rrkjr7nmXxZXXV5rAAAA HTTP/1.1" 200 -
2025-11-29 11:27:39,563 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:27:39] "GET /socket.io/?EIO=4&transport=polling&t=PhDR_kd&sid=Rrkjr7nmXxZXXV5rAAAA HTTP/1.1" 200 -
2025-11-29 11:27:39,709 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 11:27:40,780 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:27:40] "GET /idrac/api/firmware-versions HTTP/1.1" 200 -
2025-11-29 11:27:43,165 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:27:43] "GET /idrac/api/firmware-versions HTTP/1.1" 200 -
2025-11-29 11:27:47,382 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 11:27:47] "GET /socket.io/?EIO=4&transport=websocket&sid=Rrkjr7nmXxZXXV5rAAAA HTTP/1.1" 200 -
2025-11-29 11:27:49,936 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 11:28:00,164 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:19:05,980 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-29 16:19:06,006 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-29 16:19:06,006 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-29 16:19:06,078 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-29 16:19:06,096 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-29 16:19:06,096 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-29 16:19:06,096 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-29 16:19:06,096 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-29 16:19:06,145 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-29 16:19:06,146 [INFO] werkzeug: Press CTRL+C to quit
2025-11-29 16:19:07,200 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getMe "HTTP/1.1 200 OK"
2025-11-29 16:19:07,434 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/deleteWebhook "HTTP/1.1 200 OK"
2025-11-29 16:19:07,436 [INFO] telegram.ext.Application: Application started
2025-11-29 16:19:12,329 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:19:12] "GET / HTTP/1.1" 302 -
2025-11-29 16:19:12,343 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:19:12] "GET /login?next=/ HTTP/1.1" 200 -
2025-11-29 16:19:12,375 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:19:12] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 16:19:17,479 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-29 16:19:17,479 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-29 16:19:17,547 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=True
2025-11-29 16:19:17,547 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=True
2025-11-29 16:19:17,548 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-29 16:19:17,548 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-29 16:19:17,549 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:19:17] "POST /login HTTP/1.1" 302 -
2025-11-29 16:19:17,564 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:19:17] "GET /index HTTP/1.1" 200 -
2025-11-29 16:19:17,586 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:19:17] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 16:19:17,590 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:19:17] "GET /static/css/index.css HTTP/1.1" 304 -
2025-11-29 16:19:17,590 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:19:17] "GET /static/js/index.js HTTP/1.1" 304 -
2025-11-29 16:19:17,590 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:19:17] "GET /static/script.js HTTP/1.1" 304 -
2025-11-29 16:19:18,134 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:19:18,818 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-29 16:19:19,001 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:19:19] "GET /admin HTTP/1.1" 200 -
2025-11-29 16:19:19,014 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:19:19] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 16:19:19,017 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:19:19] "GET /static/js/admin.js HTTP/1.1" 304 -
2025-11-29 16:19:20,031 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:19:20] "GET /admin/settings HTTP/1.1" 500 -
2025-11-29 16:19:20,055 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:19:20] "GET /admin/settings?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 304 -
2025-11-29 16:19:20,070 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:19:20] "GET /admin/settings?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 304 -
2025-11-29 16:19:20,088 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:19:20] "GET /admin/settings?__debugger__=yes&cmd=resource&f=console.png&s=nLGik2o7oqJf9jY3fsog HTTP/1.1" 200 -
2025-11-29 16:19:28,365 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:19:38,596 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:19:48,832 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:19:59,063 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:20:09,295 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:20:19,528 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:20:29,759 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:20:39,991 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:23:51,770 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-29 16:23:51,793 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-29 16:23:51,793 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-29 16:23:51,817 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-29 16:23:51,831 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-29 16:23:51,831 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-29 16:23:51,831 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-29 16:23:51,831 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-29 16:23:51,847 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-29 16:23:51,847 [INFO] werkzeug: Press CTRL+C to quit
2025-11-29 16:23:52,589 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:23:52] "GET /admin/settings HTTP/1.1" 200 -
2025-11-29 16:23:52,671 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:23:52] "GET /static/css/admin_settings.css HTTP/1.1" 200 -
2025-11-29 16:23:52,673 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:23:52] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 16:23:52,724 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:23:52] "GET /static/favicon.ico HTTP/1.1" 304 -
2025-11-29 16:23:52,926 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getMe "HTTP/1.1 200 OK"
2025-11-29 16:23:53,156 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/deleteWebhook "HTTP/1.1 200 OK"
2025-11-29 16:23:53,158 [INFO] telegram.ext.Application: Application started
2025-11-29 16:23:59,415 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-29 16:23:59,418 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:23:59] "POST /admin/settings/bot/test/1 HTTP/1.1" 302 -
2025-11-29 16:23:59,422 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:23:59] "GET /admin/settings HTTP/1.1" 200 -
2025-11-29 16:23:59,448 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:23:59] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 16:23:59,448 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:23:59] "GET /static/css/admin_settings.css HTTP/1.1" 304 -
2025-11-29 16:24:03,847 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:24:14,079 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:24:14,607 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:14] "GET /admin HTTP/1.1" 200 -
2025-11-29 16:24:14,623 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:14] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 16:24:14,624 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:14] "GET /static/js/admin.js HTTP/1.1" 304 -
2025-11-29 16:24:15,836 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:15] "GET /admin/settings HTTP/1.1" 200 -
2025-11-29 16:24:15,852 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:15] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 16:24:15,855 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:15] "GET /static/css/admin_settings.css HTTP/1.1" 304 -
2025-11-29 16:24:17,060 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:17] "GET /admin HTTP/1.1" 200 -
2025-11-29 16:24:17,074 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:17] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 16:24:17,080 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:17] "GET /static/js/admin.js HTTP/1.1" 304 -
2025-11-29 16:24:17,684 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:17] "GET /index HTTP/1.1" 200 -
2025-11-29 16:24:17,703 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:17] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 16:24:17,705 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:17] "GET /static/css/index.css HTTP/1.1" 304 -
2025-11-29 16:24:17,708 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:17] "GET /static/js/index.js HTTP/1.1" 304 -
2025-11-29 16:24:17,709 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:17] "GET /static/script.js HTTP/1.1" 304 -
2025-11-29 16:24:18,247 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:18] "GET /xml_management HTTP/1.1" 200 -
2025-11-29 16:24:18,262 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:18] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 16:24:18,266 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:18] "GET /static/css/scp.css HTTP/1.1" 200 -
2025-11-29 16:24:18,267 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:18] "GET /static/js/scp.js HTTP/1.1" 200 -
2025-11-29 16:24:20,336 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:20] "GET /edit_xml/LinePlus_T1.xml HTTP/1.1" 200 -
2025-11-29 16:24:20,365 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:20] "GET /static/css/edit_xml.css HTTP/1.1" 200 -
2025-11-29 16:24:20,368 [INFO] werkzeug: 127.0.0.1 - - [29/Nov/2025 16:24:20] "GET /static/style.css HTTP/1.1" 304 -
2025-11-29 16:24:24,311 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:24:34,542 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:24:44,774 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:24:55,005 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:25:05,234 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:25:15,464 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:25:25,695 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:25:35,928 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:25:46,157 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:25:56,389 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:26:06,619 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:26:16,849 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:26:27,079 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:26:37,310 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:26:47,540 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:26:57,770 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:27:08,002 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:27:18,232 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:27:28,463 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:27:38,693 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:27:48,923 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:27:59,154 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:28:09,384 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:28:19,614 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:28:29,845 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:28:40,076 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:28:50,307 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:29:00,537 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:29:10,768 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:29:20,998 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:29:31,228 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-29 16:29:41,458 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"

96
data/logs/2025-11-30.log Normal file
View File

@@ -0,0 +1,96 @@
2025-11-30 07:56:33,462 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-30 07:56:33,483 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-30 07:56:33,483 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-30 07:56:33,535 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-30 07:56:33,548 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-30 07:56:33,548 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-30 07:56:33,549 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-30 07:56:33,549 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-30 07:56:33,595 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-30 07:56:33,595 [INFO] werkzeug: Press CTRL+C to quit
2025-11-30 07:56:34,624 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getMe "HTTP/1.1 200 OK"
2025-11-30 07:56:34,851 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/deleteWebhook "HTTP/1.1 200 OK"
2025-11-30 07:56:34,853 [INFO] telegram.ext.Application: Application started
2025-11-30 07:56:36,685 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:56:36] "GET / HTTP/1.1" 302 -
2025-11-30 07:56:36,714 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:56:36] "GET /login?next=/ HTTP/1.1" 200 -
2025-11-30 07:56:36,751 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:56:36] "GET /static/style.css HTTP/1.1" 304 -
2025-11-30 07:56:43,202 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-30 07:56:43,202 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-30 07:56:43,274 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=True
2025-11-30 07:56:43,274 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=True
2025-11-30 07:56:43,275 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-30 07:56:43,275 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-30 07:56:43,276 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:56:43] "POST /login HTTP/1.1" 302 -
2025-11-30 07:56:43,301 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:56:43] "GET /index HTTP/1.1" 200 -
2025-11-30 07:56:43,321 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:56:43] "GET /static/style.css HTTP/1.1" 304 -
2025-11-30 07:56:43,326 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:56:43] "GET /static/css/index.css HTTP/1.1" 200 -
2025-11-30 07:56:43,334 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:56:43] "GET /static/script.js HTTP/1.1" 304 -
2025-11-30 07:56:43,339 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:56:43] "GET /static/js/index.js HTTP/1.1" 200 -
2025-11-30 07:56:44,539 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-30 07:56:45,533 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-30 07:56:55,764 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-30 07:57:05,992 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-30 07:57:16,220 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-30 07:57:20,071 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:20] "GET /admin HTTP/1.1" 200 -
2025-11-30 07:57:20,087 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:20] "GET /static/style.css HTTP/1.1" 304 -
2025-11-30 07:57:20,091 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:20] "GET /static/js/admin.js HTTP/1.1" 200 -
2025-11-30 07:57:21,784 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:21] "GET /jobs HTTP/1.1" 200 -
2025-11-30 07:57:21,802 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:21] "GET /static/style.css HTTP/1.1" 304 -
2025-11-30 07:57:21,812 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:21] "GET /static/css/jobs.css HTTP/1.1" 200 -
2025-11-30 07:57:21,827 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:21] "GET /static/js/jobs.js HTTP/1.1" 200 -
2025-11-30 07:57:21,832 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:21] "GET /jobs/config HTTP/1.1" 200 -
2025-11-30 07:57:21,837 [WARNING] backend.services.idrac_jobs: IP list file not found: data/server_list/idrac_ip_list.txt
2025-11-30 07:57:21,837 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:21] "GET /jobs/iplist HTTP/1.1" 200 -
2025-11-30 07:57:23,860 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:23] "GET /admin HTTP/1.1" 200 -
2025-11-30 07:57:23,873 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:23] "GET /static/style.css HTTP/1.1" 304 -
2025-11-30 07:57:23,877 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:23] "GET /static/js/admin.js HTTP/1.1" 304 -
2025-11-30 07:57:26,447 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-30 07:57:27,008 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:27] "GET /admin/settings HTTP/1.1" 200 -
2025-11-30 07:57:27,021 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:27] "GET /static/style.css HTTP/1.1" 304 -
2025-11-30 07:57:27,032 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:27] "GET /static/css/admin_settings.css HTTP/1.1" 200 -
2025-11-30 07:57:35,500 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:35] "GET /jobs HTTP/1.1" 200 -
2025-11-30 07:57:35,514 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:35] "GET /static/style.css HTTP/1.1" 304 -
2025-11-30 07:57:35,517 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:35] "GET /static/css/jobs.css HTTP/1.1" 304 -
2025-11-30 07:57:35,518 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:35] "GET /static/js/jobs.js HTTP/1.1" 304 -
2025-11-30 07:57:35,537 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:35] "GET /jobs/config HTTP/1.1" 200 -
2025-11-30 07:57:35,542 [WARNING] backend.services.idrac_jobs: IP list file not found: data/server_list/idrac_ip_list.txt
2025-11-30 07:57:35,542 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:35] "GET /jobs/iplist HTTP/1.1" 200 -
2025-11-30 07:57:36,597 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:36] "GET /xml_management HTTP/1.1" 200 -
2025-11-30 07:57:36,610 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:36] "GET /static/style.css HTTP/1.1" 304 -
2025-11-30 07:57:36,615 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:36] "GET /static/css/scp.css HTTP/1.1" 200 -
2025-11-30 07:57:36,615 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:36] "GET /static/js/scp.js HTTP/1.1" 200 -
2025-11-30 07:57:36,674 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-30 07:57:40,721 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:40] "GET /edit_xml/LinePlus_T1.xml HTTP/1.1" 200 -
2025-11-30 07:57:40,734 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:40] "GET /static/style.css HTTP/1.1" 304 -
2025-11-30 07:57:40,738 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:40] "GET /static/css/edit_xml.css HTTP/1.1" 200 -
2025-11-30 07:57:42,310 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:42] "GET /xml_management HTTP/1.1" 200 -
2025-11-30 07:57:42,322 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:42] "GET /static/style.css HTTP/1.1" 304 -
2025-11-30 07:57:42,326 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:42] "GET /static/css/scp.css HTTP/1.1" 304 -
2025-11-30 07:57:42,327 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:57:42] "GET /static/js/scp.js HTTP/1.1" 304 -
2025-11-30 07:57:46,902 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-30 07:57:57,128 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-30 07:58:07,122 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:58:07] "GET /scp/diff?file1=R6615.xml&file2=R6615_raid.xml HTTP/1.1" 200 -
2025-11-30 07:58:07,138 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:58:07] "GET /static/style.css HTTP/1.1" 304 -
2025-11-30 07:58:07,140 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:58:07] "GET /static/css/scp.css HTTP/1.1" 304 -
2025-11-30 07:58:07,356 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-30 07:58:17,592 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-30 07:58:19,128 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:58:19] "GET /xml_management HTTP/1.1" 200 -
2025-11-30 07:58:19,142 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:58:19] "GET /static/style.css HTTP/1.1" 304 -
2025-11-30 07:58:19,146 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:58:19] "GET /static/css/scp.css HTTP/1.1" 304 -
2025-11-30 07:58:19,146 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:58:19] "GET /static/js/scp.js HTTP/1.1" 304 -
2025-11-30 07:58:19,647 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:58:19] "GET /index HTTP/1.1" 200 -
2025-11-30 07:58:19,661 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:58:19] "GET /static/style.css HTTP/1.1" 304 -
2025-11-30 07:58:19,667 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:58:19] "GET /static/css/index.css HTTP/1.1" 304 -
2025-11-30 07:58:19,667 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:58:19] "GET /static/js/index.js HTTP/1.1" 304 -
2025-11-30 07:58:19,668 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:58:19] "GET /static/script.js HTTP/1.1" 304 -
2025-11-30 07:58:24,733 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:58:24] "GET /home/ HTTP/1.1" 200 -
2025-11-30 07:58:24,746 [INFO] werkzeug: 127.0.0.1 - - [30/Nov/2025 07:58:24] "GET /static/style.css HTTP/1.1" 304 -
2025-11-30 07:58:27,818 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-30 07:58:38,046 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-30 07:58:48,273 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-30 07:58:58,501 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-30 07:59:08,734 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-30 07:59:18,960 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"

3824
data/logs/2025-12-18.log Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,14 @@ import subprocess
import time
from dotenv import load_dotenv
from concurrent.futures import ThreadPoolExecutor
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# .env 파일 로드
load_dotenv()
@@ -99,9 +107,9 @@ def fetch_idrac_info(ip_address):
f.write(f"01. RAID Settings - Raid ProductName : {get_value(hwinventory, 'ProductName = PERC')}\n")
f.write(f"02. RAID Settings - Raid Types : No-Raid mode\n")
print(f"IP {ip_address} 에 대한 정보를 {output_file} 에 저장했습니다.")
logging.info(f"IP {ip_address} 에 대한 정보를 {output_file} 에 저장했습니다.")
except Exception as e:
print(f"오류 발생: {e}")
logging.error(f"오류 발생: {e}")
# 명령 결과에서 원하는 값 가져오기
def get_value(output, key):
@@ -117,7 +125,7 @@ start_time = time.time()
if __name__ == "__main__":
import sys
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <ip_file>")
logging.error(f"Usage: {sys.argv[0]} <ip_file>")
sys.exit(1)
ip_file_path = validate_ip_file(sys.argv[1])
@@ -138,5 +146,5 @@ elapsed_hours = int(elapsed_time // 3600)
elapsed_minutes = int((elapsed_time % 3600) // 60)
elapsed_seconds = int(elapsed_time % 60)
print("정보 수집 완료.")
print(f"수집 완료 시간: {elapsed_hours} 시간, {elapsed_minutes} 분, {elapsed_seconds} 초.")
logging.info("정보 수집 완료.")
logging.info(f"수집 완료 시간: {elapsed_hours} 시간, {elapsed_minutes} 분, {elapsed_seconds} 초.")

View File

@@ -16,7 +16,8 @@ RACADM = os.getenv("RACADM_PATH", "racadm") # PATH에 있으면 'racadm'
# 로깅
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
def read_ip_list(ip_file: Path):
@@ -66,7 +67,7 @@ def apply_xml(ip: str, xml_path: Path) -> tuple[bool, str]:
# 대안:
# cmd = [RACADM, "-r", ip, "-u", IDRAC_USER, "-p", IDRAC_PASS, "set", "-t", "xml", "-f", str(safe_path)]
logging.info("실행 명령(리스트) → %s", " ".join(cmd))
logging.info(f"실행 명령(리스트) → {' '.join(cmd)}")
try:
p = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", shell=False, timeout=180)
stdout = (p.stdout or "").strip()
@@ -88,7 +89,7 @@ def apply_xml(ip: str, xml_path: Path) -> tuple[bool, str]:
def main():
if len(sys.argv) != 3:
print("Usage: python 02-set_config.py <ip_file> <xml_file>")
logging.error("Usage: python 02-set_config.py <ip_file> <xml_file>")
sys.exit(1)
ip_file = Path(sys.argv[1])

View File

@@ -4,6 +4,14 @@ import time
from dotenv import load_dotenv
import sys
from multiprocessing import Pool
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# 환경 변수 로드
load_dotenv() # .env 파일에서 환경 변수 로드
@@ -14,26 +22,27 @@ IDRAC_USER, IDRAC_PASS = os.getenv("IDRAC_USER"), os.getenv("IDRAC_PASS")
# IP 주소 파일 로드 함수
def load_ip_file(ip_file_path):
if not os.path.isfile(ip_file_path):
sys.exit(f"IP file {ip_file_path} does not exist.")
logging.error(f"IP file {ip_file_path} does not exist.")
sys.exit(1)
with open(ip_file_path, "r") as file:
return [line.strip() for line in file if line.strip()]
# iDRAC 정보를 가져오는 함수 정의
def fetch_idrac_info(idrac_ip):
print(f"Collecting TSR report for iDRAC IP: {idrac_ip}")
logging.info(f"Collecting TSR report for iDRAC IP: {idrac_ip}")
try:
result = subprocess.run(
["racadm", "-r", idrac_ip, "-u", IDRAC_USER, "-p", IDRAC_PASS, "techsupreport", "collect"],
capture_output=True,
text=True
)
print(
f"Successfully collected TSR report for {idrac_ip}"
if result.returncode == 0
else f"Failed to collect TSR report for {idrac_ip}: {result.stderr.strip()}"
)
if result.returncode == 0:
logging.info(f"Successfully collected TSR report for {idrac_ip}")
else:
logging.error(f"Failed to collect TSR report for {idrac_ip}: {result.stderr.strip()}")
except Exception as e:
print(f"Exception occurred for {idrac_ip}: {e}")
logging.error(f"Exception occurred for {idrac_ip}: {e}")
# 메인 함수
if __name__ == "__main__":
@@ -48,4 +57,4 @@ if __name__ == "__main__":
pool.map(fetch_idrac_info, ip_list)
elapsed_time = time.time() - start_time
print(f"설정 완료. 수집 완료 시간: {int(elapsed_time // 3600)} 시간, {int((elapsed_time % 3600) // 60)} 분, {int(elapsed_time % 60)} 초.")
logging.info(f"설정 완료. 수집 완료 시간: {int(elapsed_time // 3600)} 시간, {int((elapsed_time % 3600) // 60)} 분, {int(elapsed_time % 60)} 초.")

View File

@@ -3,6 +3,14 @@ import subprocess
from dotenv import load_dotenv
import sys
from multiprocessing import Pool
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# 환경 변수 로드
load_dotenv() # .env 파일에서 환경 변수 로드
@@ -16,13 +24,14 @@ OME_PASS = os.getenv("OME_PASS")
# IP 주소 파일 로드 및 유효성 검사
def load_ip_file(ip_file_path):
if not os.path.isfile(ip_file_path):
sys.exit(f"IP file {ip_file_path} does not exist.")
logging.error(f"IP file {ip_file_path} does not exist.")
sys.exit(1)
with open(ip_file_path, "r") as file:
return [line.strip() for line in file if line.strip()]
# iDRAC 정보를 가져오는 함수
def fetch_idrac_info(idrac_ip):
print(f"Collecting TSR report for iDRAC IP: {idrac_ip}")
logging.info(f"Collecting TSR report for iDRAC IP: {idrac_ip}")
try:
result = subprocess.run(
[
@@ -32,13 +41,13 @@ def fetch_idrac_info(idrac_ip):
capture_output=True,
text=True
)
print(
f"Successfully collected TSR report for {idrac_ip}"
if result.returncode == 0
else f"Failed to collect TSR report for {idrac_ip}: {result.stderr.strip()}"
)
if result.returncode == 0:
logging.info(f"Successfully collected TSR report for {idrac_ip}")
else:
logging.error(f"Failed to collect TSR report for {idrac_ip}: {result.stderr.strip()}")
except Exception as e:
print(f"Exception occurred for {idrac_ip}: {e}")
logging.error(f"Exception occurred for {idrac_ip}: {e}")
# 메인 함수
if __name__ == "__main__":
@@ -51,4 +60,4 @@ if __name__ == "__main__":
with Pool() as pool:
pool.map(fetch_idrac_info, ip_list)
print("설정 완료.")
logging.info("설정 완료.")

View File

@@ -4,6 +4,14 @@ import time
from dotenv import load_dotenv
import sys
from multiprocessing import Pool
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# Load environment variables
load_dotenv() # Load variables from .env file
@@ -14,26 +22,27 @@ IDRAC_USER, IDRAC_PASS = os.getenv("IDRAC_USER"), os.getenv("IDRAC_PASS")
# Load IP addresses from file
def load_ip_file(ip_file_path):
if not os.path.isfile(ip_file_path):
sys.exit(f"IP file {ip_file_path} does not exist.")
logging.error(f"IP file {ip_file_path} does not exist.")
sys.exit(1)
with open(ip_file_path, "r") as file:
return [line.strip() for line in file if line.strip()]
# Power on the server for given iDRAC IP
def poweron_idrac_server(idrac_ip):
print(f"Powering on server for iDRAC IP: {idrac_ip}")
logging.info(f"Powering on server for iDRAC IP: {idrac_ip}")
try:
result = subprocess.run(
["racadm", "-r", idrac_ip, "-u", IDRAC_USER, "-p", IDRAC_PASS, "serveraction", "powerup"],
capture_output=True,
text=True
)
print(
f"Successfully powered on server for {idrac_ip}"
if result.returncode == 0
else f"Failed to power on server for {idrac_ip}: {result.stderr.strip()}"
)
if result.returncode == 0:
logging.info(f"Successfully powered on server for {idrac_ip}")
else:
logging.error(f"Failed to power on server for {idrac_ip}: {result.stderr.strip()}")
except Exception as e:
print(f"Exception occurred for {idrac_ip}: {e}")
logging.error(f"Exception occurred for {idrac_ip}: {e}")
# Main function
if __name__ == "__main__":
@@ -48,4 +57,4 @@ if __name__ == "__main__":
pool.map(poweron_idrac_server, ip_list)
elapsed_time = time.time() - start_time
print(f"Server Power On 완료. 완료 시간: {int(elapsed_time // 3600)} 시간, {int((elapsed_time % 3600) // 60)} 분, {int(elapsed_time % 60)} 초.")
logging.info(f"Server Power On 완료. 완료 시간: {int(elapsed_time // 3600)} 시간, {int((elapsed_time % 3600) // 60)} 분, {int(elapsed_time % 60)} 초.")

View File

@@ -4,6 +4,14 @@ import time
from dotenv import load_dotenv
import sys
from multiprocessing import Pool
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# Load environment variables
load_dotenv() # Load variables from .env file
@@ -14,26 +22,27 @@ IDRAC_USER, IDRAC_PASS = os.getenv("IDRAC_USER"), os.getenv("IDRAC_PASS")
# Load IP addresses from file
def load_ip_file(ip_file_path):
if not os.path.isfile(ip_file_path):
sys.exit(f"IP file {ip_file_path} does not exist.")
logging.error(f"IP file {ip_file_path} does not exist.")
sys.exit(1)
with open(ip_file_path, "r") as file:
return [line.strip() for line in file if line.strip()]
# Power off the server for given iDRAC IP
def poweroff_idrac_server(idrac_ip):
print(f"Powering off server for iDRAC IP: {idrac_ip}")
logging.info(f"Powering off server for iDRAC IP: {idrac_ip}")
try:
result = subprocess.run(
["racadm", "-r", idrac_ip, "-u", IDRAC_USER, "-p", IDRAC_PASS, "serveraction", "powerdown"],
capture_output=True,
text=True
)
print(
f"Successfully powered off server for {idrac_ip}"
if result.returncode == 0
else f"Failed to power off server for {idrac_ip}: {result.stderr.strip()}"
)
if result.returncode == 0:
logging.info(f"Successfully powered off server for {idrac_ip}")
else:
logging.error(f"Failed to power off server for {idrac_ip}: {result.stderr.strip()}")
except Exception as e:
print(f"Exception occurred for {idrac_ip}: {e}")
logging.error(f"Exception occurred for {idrac_ip}: {e}")
# Main function
if __name__ == "__main__":
@@ -48,4 +57,4 @@ if __name__ == "__main__":
pool.map(poweroff_idrac_server, ip_list)
elapsed_time = time.time() - start_time
print(f"Server Power Off 완료. 완료 시간: {int(elapsed_time // 3600)} 시간, {int((elapsed_time % 3600) // 60)} 분, {int(elapsed_time % 60)} 초.")
logging.info(f"Server Power Off 완료. 완료 시간: {int(elapsed_time // 3600)} 시간, {int((elapsed_time % 3600) // 60)} 분, {int(elapsed_time % 60)} 초.")

View File

@@ -4,6 +4,14 @@ import time
from dotenv import load_dotenv
import sys
from multiprocessing import Pool
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# Load environment variables
load_dotenv() # Load variables from .env file
@@ -14,26 +22,27 @@ IDRAC_USER, IDRAC_PASS = os.getenv("IDRAC_USER"), os.getenv("IDRAC_PASS")
# Load IP addresses from file
def load_ip_file(ip_file_path):
if not os.path.isfile(ip_file_path):
sys.exit(f"IP file {ip_file_path} does not exist.")
logging.error(f"IP file {ip_file_path} does not exist.")
sys.exit(1)
with open(ip_file_path, "r") as file:
return [line.strip() for line in file if line.strip()]
# Delete all jobs for given iDRAC IP
def delete_all_jobs(idrac_ip):
print(f"Deleting all jobs for iDRAC IP: {idrac_ip}")
logging.info(f"Deleting all jobs for iDRAC IP: {idrac_ip}")
try:
result = subprocess.run(
["racadm", "-r", idrac_ip, "-u", IDRAC_USER, "-p", IDRAC_PASS, "jobqueue", "delete", "-i", "ALL"],
capture_output=True,
text=True
)
print(
f"Successfully deleted all jobs for {idrac_ip}"
if result.returncode == 0
else f"Failed to delete jobs for {idrac_ip}: {result.stderr.strip()}"
)
if result.returncode == 0:
logging.info(f"Successfully deleted all jobs for {idrac_ip}")
else:
logging.error(f"Failed to delete jobs for {idrac_ip}: {result.stderr.strip()}")
except Exception as e:
print(f"Exception occurred for {idrac_ip}: {e}")
logging.error(f"Exception occurred for {idrac_ip}: {e}")
# Main function
if __name__ == "__main__":
@@ -48,4 +57,4 @@ if __name__ == "__main__":
pool.map(delete_all_jobs, ip_list)
elapsed_time = time.time() - start_time
print(f"Job delete 완료. 완료 시간: {int(elapsed_time // 3600)} 시간, {int((elapsed_time % 3600) // 60)} 분, {int(elapsed_time % 60)} 초.")
logging.info(f"Job delete 완료. 완료 시간: {int(elapsed_time // 3600)} 시간, {int((elapsed_time % 3600) // 60)} 분, {int(elapsed_time % 60)} 초.")

View File

@@ -9,7 +9,11 @@ from concurrent.futures import ThreadPoolExecutor
load_dotenv()
# 로깅 설정
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)
# 사용자 이름 및 비밀번호 설정

View File

@@ -0,0 +1,206 @@
import subprocess
import sys
import re
import time
from pathlib import Path
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# ─────────────────────────────────────────────────────────────
# iDRAC 접속 설정
IDRAC_USER = "root"
IDRAC_PASS = "calvin"
# ─────────────────────────────────────────────────────────────
# 저장 위치 결정
def resolve_output_dir() -> Path:
here = Path(__file__).resolve().parent
if here.name.lower() == "scripts" and here.parent.name.lower() == "data":
base = here.parent
elif here.name.lower() == "scripts":
base = here.parent
else:
base = here.parent
out = base / "idrac_info"
out.mkdir(parents=True, exist_ok=True)
return out
# ─────────────────────────────────────────────────────────────
# 유틸리티 함수
def run_racadm(ip, *args):
"""racadm 명령어를 실행하고 결과를 반환합니다."""
cmd = ["racadm", "-r", ip, "-u", IDRAC_USER, "-p", IDRAC_PASS] + list(args)
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=30)
return result.stdout
except Exception as e:
# 에러 발생 시 빈 문자열 반환 (스크립트 중단 방지)
return ""
def extract_value(text, pattern, delimiter='='):
"""텍스트에서 특정 키의 값을 추출하고 공백을 제거합니다."""
for line in text.splitlines():
if re.search(pattern, line, re.IGNORECASE):
if delimiter in line:
# '=' 기준으로 쪼갠 후 값만 가져와서 공백 제거
return line.split(delimiter, 1)[1].strip()
return "N/A"
# ─────────────────────────────────────────────────────────────
# 메인 수집 함수
def fetch_idrac_info(ip, output_dir):
logging.info(f">>> [{ip}] 데이터 수집 시작...")
# 1. 원본 데이터 덩어리 가져오기 (API 호출 최소화)
hwinventory = run_racadm(ip, "hwinventory")
getsysinfo = run_racadm(ip, "getsysinfo")
sys_profile = run_racadm(ip, "get", "bios.SysProfileSettings")
proc_settings = run_racadm(ip, "get", "bios.ProcSettings")
mem_settings = run_racadm(ip, "get", "bios.MemSettings")
storage_ctrl = run_racadm(ip, "get", "STORAGE.Controller.1")
# 서비스 태그 확인 (파일명 결정용)
svc_tag = extract_value(getsysinfo, "SVC Tag")
if svc_tag == "N/A":
logging.error(f"!!! [{ip}] SVC Tag 추출 실패. 작업을 건너뜁니다.")
return
output_file = output_dir / f"{svc_tag}.txt"
with open(output_file, "w", encoding="utf-8") as f:
# 헤더 작성
f.write(f"Dell EMC Server Bios,iDRAC,R/C Setting (SVC Tag: {svc_tag})\n\n")
# --- 1. Firmware Version 정보 ---
f.write("-" * 42 + " Firmware Version 정보 " + "-" * 42 + "\n")
f.write(f"1. SVC Tag : {svc_tag}\n")
f.write(f"2. Bios Firmware : {extract_value(getsysinfo, 'System BIOS Version')}\n")
f.write(f"3. iDRAC Firmware Version : {extract_value(getsysinfo, 'Firmware Version')}\n")
# NIC 펌웨어 (별도 호출 필요)
nic1 = run_racadm(ip, "get", "NIC.FrmwImgMenu.1")
nic5 = run_racadm(ip, "get", "NIC.FrmwImgMenu.5")
f.write(f"4. NIC Integrated Firmware Version : {extract_value(nic1, '#FamilyVersion')}\n")
f.write(f"5. OnBoard NIC Firmware Version : {extract_value(nic5, '#FamilyVersion')}\n")
f.write(f"6. Raid Controller Firmware Version : {extract_value(hwinventory, 'ControllerFirmwareVersion')}\n\n")
# --- 2. Bios 설정 정보 ---
f.write("-" * 45 + " Bios 설정 정보 " + "-" * 45 + "\n")
boot_mode = run_racadm(ip, "get", "bios.BiosBootSettings")
f.write(f"01. Bios Boot Mode : {extract_value(boot_mode, 'BootMode')}\n")
f.write(f"02. System Profile Settings - System Profile : {extract_value(sys_profile, 'SysProfile=')}\n")
f.write(f"03. System Profile Settings - CPU Power Management : {extract_value(sys_profile, 'ProcPwrPerf')}\n")
f.write(f"04. System Profile Settings - Memory Frequency : {extract_value(sys_profile, 'MemFrequency')}\n")
f.write(f"05. System Profile Settings - Turbo Boost : {extract_value(sys_profile, 'ProcTurboMode')}\n")
f.write(f"06. System Profile Settings - PCI ASPM L1 Link Power Management : {extract_value(sys_profile, 'PcieAspmL1')}\n")
f.write(f"07. System Profile Settings - C-States : {extract_value(sys_profile, 'ProcCStates')}\n")
f.write(f"08. System Profile Settings - Determinism Slider : {extract_value(sys_profile, 'DeterminismSlider')}\n")
f.write(f"08-2. System Profile Settings - Dynamic Link Width Management (DLWM) : {extract_value(sys_profile, 'DynamicLinkWidthManagement')}\n")
f.write(f"09. Processor Settings - Logical Processor : {extract_value(proc_settings, 'LogicalProc')}\n")
f.write(f"10. Processor Settings - Virtualization Technology : {extract_value(proc_settings, 'ProcVirtualization')}\n")
f.write(f"11. Processor Settings - NUMA Nodes Per Socket : {extract_value(proc_settings, 'NumaNodesPerSocket')}\n")
f.write(f"12. Processor Settings - x2APIC Mode : {extract_value(proc_settings, 'ProcX2Apic')}\n")
f.write(f"13. Memory Settings - Dram Refresh Delay : {extract_value(mem_settings, 'DramRefreshDelay')}\n")
f.write(f"14. Memory Settings - DIMM Self Healing (PPR) : {extract_value(mem_settings, 'PPROnUCE')}\n")
f.write(f"15. Memory Settings - Correctable Error Logging : {extract_value(mem_settings, 'CECriticalSEL')}\n")
thermal = run_racadm(ip, "get", "System.ThermalSettings")
f.write(f"16. System Settings - Thermal Profile Optimization : {extract_value(thermal, 'ThermalProfile')}\n")
integrated = run_racadm(ip, "get", "Bios.IntegratedDevices")
f.write(f"17. Integrated Devices Settings - SR-IOV Global Enable : {extract_value(integrated, 'SriovGlobalEnable')}\n")
misc = run_racadm(ip, "get", "bios.MiscSettings")
f.write(f"18. Miscellaneous Settings - F1/F2 Prompt on Error : {extract_value(misc, 'ErrPrompt')}\n\n")
# --- 3. iDRAC 설정 정보 ---
f.write("-" * 45 + " iDRAC 설정 정보 " + "-" * 45 + "\n")
f.write(f"01. iDRAC Settings - Timezone : {extract_value(run_racadm(ip, 'get', 'iDRAC.Time.Timezone'), 'Timezone')}\n")
f.write(f"02. iDRAC Settings - IPMI LAN Selection : {extract_value(run_racadm(ip, 'get', 'iDRAC.CurrentNIC'), 'ActiveNIC')}\n")
f.write(f"03. iDRAC Settings - IPMI IP(IPv4) : {extract_value(run_racadm(ip, 'get', 'iDRAC.CurrentIPv4'), 'DHCPEnable')}\n")
f.write(f"04. iDRAC Settings - IPMI IP(IPv6) : {extract_value(run_racadm(ip, 'get', 'iDRAC.CurrentIPv6'), 'Enable=')}\n")
f.write(f"05. iDRAC Settings - Redfish Support : {extract_value(run_racadm(ip, 'get', 'iDRAC.Redfish.Enable'), 'Enable=')}\n")
f.write(f"06. iDRAC Settings - SSH Support : {extract_value(run_racadm(ip, 'get', 'iDRAC.SSH'), 'Enable=')}\n")
# AD 관련 설정 (AD Group 1, 2 포함)
f.write(f"07. iDRAC Settings - AD User Domain Name : {extract_value(run_racadm(ip, 'get', 'iDRAC.USERDomain.1.Name'), 'Name')}\n")
f.write(f"08. iDRAC Settings - SC Server Address : {extract_value(run_racadm(ip, 'get', 'iDRAC.ActiveDirectory.DomainController1'), 'DomainController1')}\n")
f.write(f"09. iDRAC Settings - SE AD RoleGroup Name : {extract_value(run_racadm(ip, 'get', 'iDRAC.ADGroup.1.Name'), 'Name')}\n")
f.write(f"10. iDRAC Settings - SE AD RoleGroup Domain : {extract_value(run_racadm(ip, 'get', 'iDRAC.ADGroup.1.Domain'), 'Domain')}\n")
f.write(f"11. iDRAC Settings - SE AD RoleGroup Privilege : {extract_value(run_racadm(ip, 'get', 'iDRAC.ADGroup.1.Privilege'), 'Privilege')}\n")
f.write(f"12. iDRAC Settings - IDC AD RoleGroup Name : {extract_value(run_racadm(ip, 'get', 'iDRAC.ADGroup.2.Name'), 'Name')}\n")
f.write(f"13. iDRAC Settings - IDC AD RoleGroup Domain : {extract_value(run_racadm(ip, 'get', 'iDRAC.ADGroup.2.Domain'), 'Domain')}\n")
f.write(f"14. iDRAC Settings - IDC AD RoleGroup Privilege : {extract_value(run_racadm(ip, 'get', 'iDRAC.ADGroup.2.Privilege'), 'Privilege')}\n")
# Syslog 및 기타
f.write(f"15. iDRAC Settings - Remote Log (syslog) : {extract_value(run_racadm(ip, 'get', 'iDRAC.SysLog.SysLogEnable'), 'SysLogEnable')}\n")
f.write(f"16. iDRAC Settings - syslog server address 1 : {extract_value(run_racadm(ip, 'get', 'iDRAC.SysLog.Server1'), 'Server1')}\n")
f.write(f"17. iDRAC Settings - syslog server address 2 : {extract_value(run_racadm(ip, 'get', 'iDRAC.SysLog.Server2'), 'Server2')}\n")
f.write(f"18. iDRAC Settings - syslog server port : {extract_value(run_racadm(ip, 'get', 'iDRAC.SysLog.Port'), 'Port')}\n")
f.write(f"19. iDRAC Settings - Remote KVM Nonsecure port : {extract_value(run_racadm(ip, 'get', 'iDRAC.VirtualConsole.Port'), 'Port')}\n\n")
# --- 4. Raid 설정 정보 (주석 해제 버전) ---
f.write("-" * 45 + " Raid 설정 정보 " + "-" * 45 + "\n")
f.write(f"01. RAID Settings - Raid ProductName : {extract_value(hwinventory, 'ProductName = PERC')}\n")
f.write(f"02. RAID Settings - Raid Types : {extract_value(hwinventory, 'RAIDTypes')}\n")
f.write(f"03. RAID Settings - StripeSize : {extract_value(hwinventory, 'StripeSize')}\n")
f.write(f"04. RAID Settings - ReadCachePolicy : {extract_value(hwinventory, 'ReadCachePolicy')}\n")
f.write(f"05. RAID Settings - WriteCachePolicy : {extract_value(hwinventory, 'WriteCachePolicy')}\n")
f.write(f"06. RAID Settings - CheckConsistencyRate : {extract_value(storage_ctrl, 'CheckConsistencyRate')}\n")
f.write(f"07. RAID Settings - PatrolReadMode : {extract_value(storage_ctrl, 'PatrolReadMode')}\n")
f.write(f"08. RAID Settings - period : 168h\n")
f.write(f"09. RAID Settings - Power Save : No\n")
f.write(f"10. RAID Settings - JBODMODE : Controller does not support JBOD\n")
f.write(f"11. RAID Settings - maxconcurrentpd : 240\n")
logging.info(f"✅ [{ip}] 저장 완료: {output_file.name}")
# ─────────────────────────────────────────────────────────────
# 실행 흐름 제어
def main():
if len(sys.argv) < 2:
logging.error(f"사용법: python {sys.argv[0]} <ip_file>")
sys.exit(1)
ip_file = Path(sys.argv[1])
if not ip_file.exists():
logging.error(f"오류: IP 파일 '{ip_file}'이 존재하지 않습니다.")
sys.exit(1)
output_dir = resolve_output_dir()
start_time = time.time()
# IP 목록 읽기
with open(ip_file, "r") as f:
ips = [line.strip() for line in f if line.strip()]
# 각 IP별로 수집 실행
for ip in ips:
try:
fetch_idrac_info(ip, output_dir)
except Exception as e:
logging.error(f"❌ [{ip}] 처리 중 예외 발생: {e}")
# 소요 시간 계산
end_time = time.time()
elapsed = end_time - start_time
hours, rem = divmod(elapsed, 3600)
minutes, seconds = divmod(rem, 60)
logging.info("="*50)
logging.info("정보 수집 완료.")
logging.info(f"총 소요 시간: {int(hours)}시간 {int(minutes)}{int(seconds)}")
logging.info(f"저장 경로: {output_dir}")
logging.info("="*50)
if __name__ == "__main__":
main()

View File

@@ -3,9 +3,17 @@ import re
import subprocess
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
import logging
from dotenv import load_dotenv, find_dotenv
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# ─────────────────────────────────────────────────────────────
# .env 자동 탐색 로드 (현재 파일 기준 상위 디렉터리까지 검색)
load_dotenv(find_dotenv())
@@ -86,7 +94,7 @@ def fetch_idrac_info(idrac_ip: str, output_dir: Path) -> None:
svc_tag = svc_tag_match.group(1) if svc_tag_match else None
if not svc_tag:
print(f"Failed to retrieve SVC Tag for IP: {idrac_ip}")
logging.error(f"Failed to retrieve SVC Tag for IP: {idrac_ip}")
return
# 전체 하드웨어 인벤토리
@@ -125,13 +133,13 @@ def fetch_idrac_info(idrac_ip: str, output_dir: Path) -> None:
f.write(f"SERIALS: {';'.join(serials_only)}\n")
except Exception as e:
print(f"Error processing IP {idrac_ip}: {e}")
logging.error(f"Error processing IP {idrac_ip}: {e}")
def main(ip_file: str) -> None:
ip_path = Path(ip_file)
if not ip_path.is_file():
print(f"IP file {ip_file} does not exist.")
logging.error(f"IP file {ip_file} does not exist.")
return
output_dir = resolve_output_dir()
@@ -146,14 +154,14 @@ def main(ip_file: str) -> None:
ip = future_to_ip[future]
try:
future.result()
print(f"✅ Completed: {ip}")
logging.info(f"✅ Completed: {ip}")
except Exception as e:
print(f"❌ Error processing {ip}: {e}")
logging.error(f"❌ Error processing {ip}: {e}")
if __name__ == "__main__":
import sys
if len(sys.argv) != 2:
print("Usage: python GPU_Serial_v1.py <ip_file>")
logging.error("Usage: python GPU_Serial_v1.py <ip_file>")
sys.exit(1)
main(sys.argv[1])

View File

@@ -0,0 +1,192 @@
import subprocess
import os
import re
import time
from pathlib import Path
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# --- [설정] iDRAC 접속 정보 ---
IDRAC_USER = "root"
IDRAC_PASS = "calvin"
def resolve_output_dir() -> Path:
"""
사용자가 지정한 로직에 따라 저장 위치를 결정합니다.
스크립트 위치가 /data/scripts/ 일 경우 /data/idrac_info/ 에 저장합니다.
"""
here = Path(__file__).resolve().parent
if here.name.lower() == "scripts" and here.parent.name.lower() == "data":
base = here.parent
elif here.name.lower() == "scripts":
base = here.parent
else:
base = here.parent
out = base / "idrac_info"
out.mkdir(parents=True, exist_ok=True)
return out
def run_racadm(ip: str, command: str) -> str:
"""racadm 명령어를 실행하고 결과를 문자열로 반환합니다."""
full_cmd = f"racadm -r {ip} -u {IDRAC_USER} -p {IDRAC_PASS} {command}"
try:
# 쉘 명령 실행 (stderr도 포함하여 수집)
result = subprocess.check_output(full_cmd, shell=True, stderr=subprocess.STDOUT, universal_newlines=True)
return result
except subprocess.CalledProcessError as e:
# 통신 실패 시 빈 문자열 반환
return ""
def get_val(text: str, pattern: str) -> str:
"""텍스트 내에서 특정 키워드를 찾아 해당 줄의 설정값(= 이후의 값)을 추출합니다."""
for line in text.splitlines():
if re.search(pattern, line, re.IGNORECASE):
parts = line.split('=')
if len(parts) >= 2:
return parts[1].strip()
return "N/A"
def fetch_idrac_info(ip: str, output_dir: Path):
logging.info(f">>> {ip} 정보 수집 중...")
# 1. 원시 데이터 벌크 수집 (네트워크 오버헤드 감소)
getsysinfo = run_racadm(ip, "getsysinfo")
hwinventory = run_racadm(ip, "hwinventory")
sys_profile = run_racadm(ip, "get bios.SysProfileSettings")
proc_settings = run_racadm(ip, "get bios.ProcSettings")
mem_settings = run_racadm(ip, "get bios.MemSettings")
storage_ctrl = run_racadm(ip, "get STORAGE.Controller.1")
# 서비스 태그 추출 (파일명 결정용)
svc_tag = get_val(getsysinfo, "SVC Tag")
if svc_tag == "N/A":
logging.error(f"[경고] {ip} 접속 실패 혹은 SVC Tag 확인 불가. 건너뜁니다.")
return
report_path = output_dir / f"{svc_tag}.txt"
with open(report_path, "w", encoding="utf-8") as f:
# 헤더 기록
f.write(f"Dell EMC Server Bios,iDRAC,R/C Setting (SVC Tag: {svc_tag})\n\n")
# --- Section 1: Firmware Version 정보 ---
f.write("-" * 42 + " Firmware Version 정보 " + "-" * 42 + "\n")
f.write(f"1. SVC Tag : {svc_tag}\n")
f.write(f"2. Bios Firmware : {get_val(getsysinfo, 'System BIOS Version')}\n")
f.write(f"3. iDRAC Firmware Version : {get_val(getsysinfo, 'Firmware Version')}\n")
nic1 = run_racadm(ip, "get NIC.FrmwImgMenu.1")
f.write(f"4. NIC Integrated Firmware Version : {get_val(nic1, '#FamilyVersion')}\n")
nic5 = run_racadm(ip, "get NIC.FrmwImgMenu.5")
f.write(f"5. OnBoard NIC Firmware Version : {get_val(nic5, '#FamilyVersion')}\n")
f.write(f"6. Raid Controller Firmware Version : {get_val(hwinventory, 'ControllerFirmwareVersion')}\n\n")
# --- Section 2: Bios 설정 정보 ---
f.write("-" * 45 + " Bios 설정 정보 " + "-" * 46 + "\n")
boot_settings = run_racadm(ip, "get bios.BiosBootSettings")
f.write(f"01. Bios Boot Mode : {get_val(boot_settings, 'BootMode')}\n")
f.write(f"02. System Profile : {get_val(sys_profile, 'SysProfile=')}\n")
f.write(f"03. CPU Power Management : {get_val(sys_profile, 'EnergyPerformanceBias')}\n")
f.write(f"04. Memory Frequency : {get_val(sys_profile, 'MemFrequency')}\n")
f.write(f"05. Turbo Boost : {get_val(sys_profile, 'ProcTurboMode')}\n")
f.write(f"06. C1E : {get_val(sys_profile, 'ProcC1E')}\n")
f.write(f"07. C-States : {get_val(sys_profile, 'ProcCStates')}\n")
f.write(f"08. Monitor/Mwait : {get_val(sys_profile, 'MonitorMwait')}\n")
f.write(f"09. Logical Processor : {get_val(proc_settings, 'LogicalProc')}\n")
f.write(f"10. Virtualization Technology : {get_val(proc_settings, 'ProcVirtualization')}\n")
f.write(f"11. LLC Prefetch : {get_val(proc_settings, 'LlcPrefetch')}\n")
f.write(f"12. x2APIC Mode : {get_val(proc_settings, 'ProcX2Apic')}\n")
f.write(f"13. Node Interleaving : {get_val(mem_settings, 'NodeInterleave')}\n")
f.write(f"14. DIMM Self Healing : {get_val(mem_settings, 'PPROnUCE')}\n")
f.write(f"15. Correctable Error Logging : {get_val(mem_settings, 'CECriticalSEL')}\n")
thermal = run_racadm(ip, "get System.ThermalSettings")
f.write(f"16. Thermal Profile Optimization : {get_val(thermal, 'ThermalProfile')}\n")
sriov = run_racadm(ip, "get Bios.IntegratedDevices")
f.write(f"17. SR-IOV Global Enable : {get_val(sriov, 'SriovGlobalEnable')}\n")
misc = run_racadm(ip, "get bios.MiscSettings")
f.write(f"18. F1/F2 Prompt on Error : {get_val(misc, 'ErrPrompt')}\n\n")
# --- Section 3: iDRAC 설정 정보 ---
f.write("-" * 45 + " iDRAC 설정 정보 " + "-" * 45 + "\n")
f.write(f"01. Timezone : {get_val(run_racadm(ip, 'get iDRAC.Time.Timezone'), 'Timezone')}\n")
f.write(f"02. IPMI LAN Selection : {get_val(run_racadm(ip, 'get iDRAC.CurrentNIC'), 'ActiveNIC')}\n")
f.write(f"03. IPMI IP(IPv4) DHCP : {get_val(run_racadm(ip, 'get iDRAC.CurrentIPv4'), 'DHCPEnable')}\n")
f.write(f"04. IPMI IP(IPv6) Enable : {get_val(run_racadm(ip, 'get iDRAC.CurrentIPv6'), 'Enable=')}\n")
f.write(f"05. Redfish Support : {get_val(run_racadm(ip, 'get iDRAC.Redfish.Enable'), 'Enable=')}\n")
f.write(f"06. SSH Support : {get_val(run_racadm(ip, 'get iDRAC.SSH'), 'Enable=')}\n")
f.write(f"07. AD User Domain Name : {get_val(run_racadm(ip, 'get iDRAC.USERDomain.1.Name'), 'Name')}\n")
f.write(f"08. SC Server Address : {get_val(run_racadm(ip, 'get iDRAC.ActiveDirectory.DomainController1'), 'DomainController1')}\n")
# Syslog 관련
f.write(f"15. Remote Log (syslog) : {get_val(run_racadm(ip, 'get iDRAC.SysLog.SysLogEnable'), 'SysLogEnable')}\n")
f.write(f"16. syslog server 1 : {get_val(run_racadm(ip, 'get iDRAC.SysLog.Server1'), 'Server1')}\n")
f.write(f"17. syslog server 2 : {get_val(run_racadm(ip, 'get iDRAC.SysLog.Server2'), 'Server2')}\n")
f.write(f"18. syslog server port : {get_val(run_racadm(ip, 'get iDRAC.SysLog.Port'), 'Port')}\n")
f.write(f"19. VirtualConsole Port : {get_val(run_racadm(ip, 'get iDRAC.VirtualConsole.Port'), 'Port')}\n\n")
# --- Section 4: Raid 설정 정보 ---
f.write("-" * 45 + " Raid 설정 정보 " + "-" * 46 + "\n")
f.write(f"01. Raid ProductName : {get_val(hwinventory, 'ProductName = BOSS')}, {get_val(hwinventory, 'ProductName = PERC')}\n")
f.write(f"02. RAID Types : {get_val(hwinventory, 'RAIDTypes')}\n")
f.write(f"03. StripeSize : {get_val(hwinventory, 'StripeSize')}\n")
f.write(f"04. ReadCachePolicy : {get_val(hwinventory, 'ReadCachePolicy')}\n")
f.write(f"05. WriteCachePolicy : {get_val(hwinventory, 'WriteCachePolicy')}\n")
f.write(f"06. CheckConsistencyMode : {get_val(storage_ctrl, 'CheckConsistencyMode')}\n")
f.write(f"07. PatrolReadRate : {get_val(storage_ctrl, 'PatrolReadRate')}\n")
f.write(f"08. period : 168h\n")
f.write(f"09. Power Save : No\n")
f.write(f"10. JBODMODE : Controller does not support JBOD\n")
f.write(f"11. maxconcurrentpd : 240\n")
logging.info(f" ㄴ 완료: {report_path.name}")
def main():
import sys
if len(sys.argv) < 2:
logging.error("Usage: python script.py <ip_list_file>")
return
ip_file = sys.argv[1]
if not os.path.exists(ip_file):
logging.error(f"파일을 찾을 수 없습니다: {ip_file}")
return
# 저장 위치 결정
output_dir = resolve_output_dir()
logging.info(f"[*] 결과 저장 폴더: {output_dir}")
# 시간 측정 시작
start_time = time.time()
# IP 파일 읽기
with open(ip_file, "r") as f:
ips = [line.strip() for line in f if line.strip()]
# 순차적 정보 수집
for ip in ips:
fetch_idrac_info(ip, output_dir)
# 소요 시간 계산
elapsed = time.time() - start_time
hours = int(elapsed // 3600)
minutes = int((elapsed % 3600) // 60)
seconds = int(elapsed % 60)
logging.info("=" * 50)
logging.info(f"정보 수집 완료.")
logging.info(f"소요 시간: {hours}시간 {minutes}{seconds}")
logging.info("=" * 50)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,174 @@
#!/usr/bin/env python3
from __future__ import annotations
import re
import subprocess
from pathlib import Path
from typing import Optional
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# ─────────────────────────────────────────────────────────────
# 저장 위치: data/scripts 아래에 있다면 → data/idrac_info 생성
def resolve_output_dir() -> Path:
here = Path(__file__).resolve().parent
base = here.parent if here.name.lower() == "scripts" else here
out = base / "idrac_info"
out.mkdir(parents=True, exist_ok=True)
return out
# ─────────────────────────────────────────────────────────────
# iDRAC 접속 계정
IDRAC_USER = "root"
IDRAC_PASS = "calvin"
def run(cmd: list[str]) -> str:
"""racadm 명령 실행 (stdout 수집)"""
try:
return subprocess.getoutput(" ".join(cmd))
except Exception:
return ""
def parse_single_value(pattern: str, text: str) -> Optional[str]:
"""단일 값 추출용"""
m = re.search(pattern, text, flags=re.IGNORECASE)
return m.group(1).strip() if m else None
def find_mac_by_fqdd(fqdd: str, text: str) -> Optional[str]:
"""
swinventory 출력에서 FQDD = {fqdd} 라인을 찾고,
그 주변(±10줄) 내에서 MAC 주소를 추출.
"""
lines = text.splitlines()
for i, line in enumerate(lines):
if f"FQDD = {fqdd}" in line:
for j in range(max(0, i - 10), min(i + 10, len(lines))):
m = re.search(r"([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})", lines[j])
if m:
return m.group(1)
return None
def extract_vendors(hwinventory: str) -> tuple[str, str]:
"""
[InstanceID: DIMM...] 또는 [InstanceID: Disk.Bay...] 블록 내 Manufacturer 추출
"""
# Memory Vendors
mem_vendors = re.findall(
r"\[InstanceID:\s*DIMM[^\]]*\][^\[]*?Manufacturer\s*=\s*([^\n\r]+)",
hwinventory,
flags=re.IGNORECASE
)
mem_vendors = [v.strip() for v in mem_vendors if v.strip()]
memory = ", ".join(sorted(set(mem_vendors))) if mem_vendors else "Not Found"
# SSD Vendors
ssd_vendors = re.findall(
r"\[InstanceID:\s*Disk\.Bay[^\]]*\][^\[]*?Manufacturer\s*=\s*([^\n\r]+)",
hwinventory,
flags=re.IGNORECASE
)
ssd_vendors = [v.strip() for v in ssd_vendors if v.strip()]
ssd = ", ".join(sorted(set(ssd_vendors))) if ssd_vendors else "Not Found"
return memory, ssd
def fetch_idrac_info_one(ip: str, output_dir: Path) -> None:
"""단일 서버 iDRAC 정보 수집"""
logging.info(f"[+] Collecting iDRAC info from {ip} ...")
getsysinfo = run(["racadm", "-r", ip, "-u", IDRAC_USER, "-p", IDRAC_PASS, "getsysinfo"])
hwinventory = run(["racadm", "-r", ip, "-u", IDRAC_USER, "-p", IDRAC_PASS, "hwinventory"])
swinventory = run(["racadm", "-r", ip, "-u", IDRAC_USER, "-p", IDRAC_PASS, "swinventory"])
# 서비스 태그
svc_tag = parse_single_value(r"SVC\s*Tag\s*=\s*(\S+)", getsysinfo)
if not svc_tag:
logging.error(f"[!] Failed to retrieve SVC Tag for IP: {ip}")
return
# iDRAC MAC
idrac_mac = parse_single_value(r"MAC\s*Address\s*=\s*([0-9A-Fa-f:]{17})", getsysinfo)
# NIC.Integrated / Onboard MAC
integrated_1 = parse_single_value(r"NIC\.Integrated\.1-1-1\s+Ethernet\s*=\s*([0-9A-Fa-f:]{17})", getsysinfo)
integrated_2 = parse_single_value(r"NIC\.Integrated\.1-2-1\s+Ethernet\s*=\s*([0-9A-Fa-f:]{17})", getsysinfo)
onboard_1 = parse_single_value(r"NIC\.Embedded\.1-1-1\s+Ethernet\s*=\s*([0-9A-Fa-f:]{17})", getsysinfo)
onboard_2 = parse_single_value(r"NIC\.Embedded\.2-1-1\s+Ethernet\s*=\s*([0-9A-Fa-f:]{17})", getsysinfo)
# NIC Slot별 MAC
NIC_Slot_2_1 = find_mac_by_fqdd("NIC.Slot.2-1-1", swinventory)
NIC_Slot_2_2 = find_mac_by_fqdd("NIC.Slot.2-2-1", swinventory)
NIC_Slot_3_1 = find_mac_by_fqdd("NIC.Slot.3-1-1", swinventory)
NIC_Slot_3_2 = find_mac_by_fqdd("NIC.Slot.3-2-1", swinventory)
# 메모리 / SSD 제조사
memory, ssd = extract_vendors(hwinventory)
# 결과 파일 저장
out_file = output_dir / f"{svc_tag}.txt"
with out_file.open("w", encoding="utf-8", newline="\n") as f:
f.write(f"{svc_tag}\n")
f.write(f"{integrated_1 or ''}\n")
f.write(f"{integrated_2 or ''}\n")
f.write(f"{onboard_1 or ''}\n")
f.write(f"{onboard_2 or ''}\n")
f.write(f"{NIC_Slot_2_1 or ''}\n")
f.write(f"{NIC_Slot_2_2 or ''}\n")
f.write(f"{idrac_mac or ''}\n")
f.write(f"{memory}\n")
f.write(f"{ssd}\n")
logging.info(f"[✔] {svc_tag} ({ip}) info saved.")
def main(ip_file: str) -> None:
ip_path = Path(ip_file)
if not ip_path.is_file():
logging.error(f"[!] IP file {ip_file} does not exist.")
return
output_dir = resolve_output_dir()
ips = [line.strip() for line in ip_path.read_text(encoding="utf-8").splitlines() if line.strip()]
if not ips:
logging.error("[!] No IP addresses found in the file.")
return
for ip in ips:
try:
fetch_idrac_info_one(ip, output_dir)
except Exception as e:
logging.error(f"[!] Error processing {ip}: {e}")
try:
ip_path.unlink(missing_ok=True)
except Exception:
pass
logging.info("\n정보 수집 완료.")
if __name__ == "__main__":
import sys, time
if len(sys.argv) != 2:
logging.error("Usage: python script.py <ip_file>")
sys.exit(1)
start = time.time()
main(sys.argv[1])
end = time.time()
elapsed = int(end - start)
h, m, s = elapsed // 3600, (elapsed % 3600) // 60, elapsed % 60
logging.info(f"수집 완료 시간: {h}시간 {m}{s}")

234
data/scripts/MAC_info.py Normal file
View File

@@ -0,0 +1,234 @@
#!/usr/bin/env python3
from __future__ import annotations
import re
import subprocess
from pathlib import Path
from typing import Optional, List
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# ─────────────────────────────────────────────────────────────
# 출력 디렉토리 결정
def resolve_output_dir() -> Path:
here = Path(__file__).resolve().parent
if here.name.lower() == "scripts" and here.parent.name.lower() == "data":
base = here.parent
else:
base = here.parent
out = base / "idrac_info"
out.mkdir(parents=True, exist_ok=True)
return out
# ─────────────────────────────────────────────────────────────
# iDRAC 계정
IDRAC_USER = "root"
IDRAC_PASS = "calvin"
def run(cmd: list[str]) -> str:
"""racadm 명령 실행 (stdout만 반환)"""
try:
return subprocess.getoutput(" ".join(cmd))
except Exception:
return ""
def parse_single_value(pattern: str, text: str) -> Optional[str]:
m = re.search(pattern, text, flags=re.IGNORECASE)
return m.group(1).strip() if m else None
# ─────────────────────────────────────────────────────────────
# NIC MAC 수집 (가변 포트 대응)
def extract_nic_macs(text: str, nic_type: str) -> List[str]:
"""
nic_type:
- 'Integrated'
- 'Embedded'
"""
pattern = re.compile(
rf"NIC\.{nic_type}\.\d+-\d+-\d+.*?([0-9A-Fa-f]{{2}}(?::[0-9A-Fa-f]{{2}}){{5}})",
re.IGNORECASE
)
return sorted(set(m.group(1) for m in pattern.finditer(text)))
# ─────────────────────────────────────────────────────────────
# hwinventory 블록 기반 Manufacturer 추출 (DIMM)
def extract_memory_vendors(hwinventory: str) -> List[str]:
vendors = set()
blocks = re.split(r"\n\s*\n", hwinventory)
for block in blocks:
if "[InstanceID: DIMM." in block:
m = re.search(r"Manufacturer\s*=\s*(.+)", block, re.IGNORECASE)
if m:
vendors.add(m.group(1).strip())
return sorted(vendors)
# ─────────────────────────────────────────────────────────────
# BOSS 디스크 Vendor 추출
def extract_boss_vendors(hwinventory: str) -> List[str]:
vendors = set()
blocks = re.split(r"\n\s*\n", hwinventory)
for block in blocks:
if "BOSS." in block:
m = re.search(r"Manufacturer\s*=\s*(.+)", block, re.IGNORECASE)
if m:
vendors.add(m.group(1).strip())
return sorted(vendors)
# ─────────────────────────────────────────────────────────────
# 일반 Disk Vendor (BOSS 제외)
DISK_PREFIXES = [
"Disk.Bay.",
"Disk.M.2.",
"Disk.Direct.",
"Disk.Slot."
]
def extract_disk_vendors_excluding_boss(hwinventory: str) -> List[str]:
vendors = set()
blocks = re.split(r"\n\s*\n", hwinventory)
for block in blocks:
if "BOSS." in block:
continue
for prefix in DISK_PREFIXES:
if f"[InstanceID: {prefix}" in block:
m = re.search(r"Manufacturer\s*=\s*(.+)", block, re.IGNORECASE)
if m:
vendors.add(m.group(1).strip())
return sorted(vendors)
# ─────────────────────────────────────────────────────────────
def fetch_idrac_info_one(ip: str, output_dir: Path) -> None:
getsysinfo = run([
"racadm", "-r", ip,
"-u", IDRAC_USER,
"-p", IDRAC_PASS,
"getsysinfo"
])
hwinventory = run([
"racadm", "-r", ip,
"-u", IDRAC_USER,
"-p", IDRAC_PASS,
"hwinventory"
])
# ── Service Tag
svc_tag = parse_single_value(r"SVC\s*Tag\s*=\s*(\S+)", getsysinfo)
if not svc_tag:
logging.error(f"[ERROR] SVC Tag 수집 실패: {ip}")
return
# ── iDRAC MAC
idrac_mac = parse_single_value(
r"MAC Address\s*=\s*([0-9A-Fa-f:]{17})", getsysinfo
)
# ── NIC MAC
integrated_macs = extract_nic_macs(getsysinfo, "Integrated")
embedded_macs = extract_nic_macs(getsysinfo, "Embedded")
# ── Vendors
memory_vendors = extract_memory_vendors(hwinventory)
disk_vendors = extract_disk_vendors_excluding_boss(hwinventory)
boss_vendors = extract_boss_vendors(hwinventory)
# ── 결과 파일 저장
out_file = output_dir / f"{svc_tag}.txt"
with out_file.open("w", encoding="utf-8", newline="\n") as f:
f.write(f"{svc_tag}\n")
for mac in integrated_macs:
f.write(f"{mac}\n")
for mac in embedded_macs:
f.write(f"{mac}\n")
f.write(f"{idrac_mac or ''}\n")
# Memory Vendors
for v in memory_vendors:
f.write(f"{v}\n")
# Disk Vendors (일반)
for v in disk_vendors:
f.write(f"{v}\n")
# BOSS Vendors (있을 때만, 벤더명만)
for v in boss_vendors:
f.write(f"{v}\n")
logging.info(f"[OK] {svc_tag} 수집 완료")
# ─────────────────────────────────────────────────────────────
def main(ip_file: str) -> None:
ip_path = Path(ip_file)
if not ip_path.is_file():
logging.error(f"[ERROR] IP 파일 없음: {ip_file}")
return
output_dir = resolve_output_dir()
ips = [
line.strip()
for line in ip_path.read_text(encoding="utf-8").splitlines()
if line.strip()
]
if not ips:
logging.error("[ERROR] IP 목록이 비어 있습니다.")
return
for ip in ips:
try:
fetch_idrac_info_one(ip, output_dir)
except Exception as e:
logging.error(f"[ERROR] {ip} 처리 실패: {e}")
# Bash 스크립트와 동일하게 입력 IP 파일 삭제
try:
ip_path.unlink(missing_ok=True)
except Exception:
pass
logging.info("정보 수집 완료.")
# ─────────────────────────────────────────────────────────────
if __name__ == "__main__":
import sys
import time
if len(sys.argv) != 2:
logging.error("Usage: python script.py <ip_file>")
sys.exit(1)
start = time.time()
main(sys.argv[1])
elapsed = int(time.time() - start)
h, m, s = elapsed // 3600, (elapsed % 3600) // 60, elapsed % 60
logging.info(f"수집 완료 시간: {h} 시간, {m} 분, {s} 초.")

View File

@@ -3,6 +3,14 @@ import re
import subprocess
from dotenv import load_dotenv
from concurrent.futures import ThreadPoolExecutor, as_completed
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# .env 파일에서 사용자 이름 및 비밀번호 설정
load_dotenv()
@@ -18,7 +26,7 @@ def fetch_idrac_info(idrac_ip, output_dir):
svc_tag = svc_tag_match.group(1) if svc_tag_match else None
if not svc_tag:
print(f"Failed to retrieve SVC Tag for IP: {idrac_ip}")
logging.error(f"Failed to retrieve SVC Tag for IP: {idrac_ip}")
return
# InfiniBand.VndrConfigPage 목록 가져오기
@@ -59,12 +67,14 @@ def fetch_idrac_info(idrac_ip, output_dir):
if hex_guid_list:
f.write(f"GUID: {';'.join(hex_guid_list)}\n")
logging.info(f"✅ Completed: {idrac_ip}")
except Exception as e:
print(f"Error processing IP {idrac_ip}: {e}")
logging.error(f"Error processing IP {idrac_ip}: {e}")
def main(ip_file):
if not os.path.isfile(ip_file):
print(f"IP file {ip_file} does not exist.")
logging.error(f"IP file {ip_file} does not exist.")
return
output_dir = "/app/idrac_info/idrac_info"
@@ -80,12 +90,12 @@ def main(ip_file):
try:
future.result()
except Exception as e:
print(f"Error processing task: {e}")
logging.error(f"Error processing task: {e}")
if __name__ == "__main__":
import sys
if len(sys.argv) != 2:
print("Usage: python script.py <ip_file>")
logging.error("Usage: python script.py <ip_file>")
sys.exit(1)
ip_file = sys.argv[1]

View File

@@ -3,9 +3,17 @@ import re
import subprocess
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
import logging
from dotenv import load_dotenv, find_dotenv
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# ─────────────────────────────────────────────────────────────
# .env 자동 탐색 로드 (현재 파일 기준 상위 디렉터리까지 검색)
load_dotenv(find_dotenv())
@@ -47,7 +55,7 @@ def fetch_idrac_info(idrac_ip: str, output_dir: Path) -> None:
svc_tag = svc_tag_match.group(1) if svc_tag_match else None
if not svc_tag:
print(f"Failed to retrieve SVC Tag for IP: {idrac_ip}")
logging.error(f"Failed to retrieve SVC Tag for IP: {idrac_ip}")
return
# InfiniBand.VndrConfigPage 목록 가져오기
@@ -110,13 +118,13 @@ def fetch_idrac_info(idrac_ip: str, output_dir: Path) -> None:
f.write(f"GUID: {';'.join(hex_guid_list)}\n")
except Exception as e:
print(f"Error processing IP {idrac_ip}: {e}")
logging.error(f"Error processing IP {idrac_ip}: {e}")
def main(ip_file: str) -> None:
ip_path = Path(ip_file)
if not ip_path.is_file():
print(f"IP file {ip_file} does not exist.")
logging.error(f"IP file {ip_file} does not exist.")
return
output_dir = resolve_output_dir() # ← 여기서 OS 무관 저장 위치 확정 (data/idrac_info)
@@ -133,15 +141,15 @@ def main(ip_file: str) -> None:
ip = future_to_ip[future]
try:
future.result()
print(f"Completed: {ip}")
logging.info(f"Completed: {ip}")
except Exception as e:
print(f"Error processing {ip}: {e}")
logging.error(f"Error processing {ip}: {e}")
if __name__ == "__main__":
import sys
if len(sys.argv) != 2:
print("Usage: python script.py <ip_file>")
logging.error("Usage: python script.py <ip_file>")
sys.exit(1)
main(sys.argv[1])

View File

@@ -0,0 +1,166 @@
#!/usr/bin/env python3
from __future__ import annotations
import re
import subprocess
from pathlib import Path
from typing import Optional
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# ─────────────────────────────────────────────────────────────
# 저장 위치: data/scripts 아래에 있다면 → data/idrac_info 생성
def resolve_output_dir() -> Path:
here = Path(__file__).resolve().parent
base = here.parent if here.name.lower() == "scripts" else here
out = base / "idrac_info"
out.mkdir(parents=True, exist_ok=True)
return out
# ─────────────────────────────────────────────────────────────
# iDRAC 접속 계정
IDRAC_USER = "root"
IDRAC_PASS = "calvin"
def run(cmd: list[str]) -> str:
"""racadm 명령 실행 (stdout 수집)"""
try:
return subprocess.getoutput(" ".join(cmd))
except Exception:
return ""
def parse_single_value(pattern: str, text: str) -> Optional[str]:
"""단일 값 추출용"""
m = re.search(pattern, text, flags=re.IGNORECASE)
return m.group(1).strip() if m else None
def find_mac_by_fqdd(fqdd: str, text: str) -> Optional[str]:
"""
swinventory 출력에서 FQDD = {fqdd} 라인을 찾고,
그 주변(±10줄) 내에서 MAC 주소를 추출.
"""
lines = text.splitlines()
for i, line in enumerate(lines):
if f"FQDD = {fqdd}" in line:
for j in range(max(0, i - 10), min(i + 10, len(lines))):
m = re.search(r"([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})", lines[j])
if m:
return m.group(1)
return None
def extract_vendors(hwinventory: str) -> tuple[str, str]:
"""
[InstanceID: DIMM...] 또는 [InstanceID: Disk.Bay...] 블록 내 Manufacturer 추출
"""
# Memory Vendors
mem_vendors = re.findall(
r"\[InstanceID:\s*DIMM[^\]]*\][^\[]*?Manufacturer\s*=\s*([^\n\r]+)",
hwinventory,
flags=re.IGNORECASE
)
mem_vendors = [v.strip() for v in mem_vendors if v.strip()]
memory = ", ".join(sorted(set(mem_vendors))) if mem_vendors else "Not Found"
# SSD Vendors
ssd_vendors = re.findall(
r"\[InstanceID:\s*Disk\.Bay[^\]]*\][^\[]*?Manufacturer\s*=\s*([^\n\r]+)",
hwinventory,
flags=re.IGNORECASE
)
ssd_vendors = [v.strip() for v in ssd_vendors if v.strip()]
ssd = ", ".join(sorted(set(ssd_vendors))) if ssd_vendors else "Not Found"
return memory, ssd
def fetch_idrac_info_one(ip: str, output_dir: Path) -> None:
"""단일 서버 iDRAC 정보 수집"""
logging.info(f"[+] Collecting iDRAC info from {ip} ...")
getsysinfo = run(["racadm", "-r", ip, "-u", IDRAC_USER, "-p", IDRAC_PASS, "getsysinfo"])
hwinventory = run(["racadm", "-r", ip, "-u", IDRAC_USER, "-p", IDRAC_PASS, "hwinventory"])
swinventory = run(["racadm", "-r", ip, "-u", IDRAC_USER, "-p", IDRAC_PASS, "swinventory"])
# 서비스 태그
svc_tag = parse_single_value(r"SVC\s*Tag\s*=\s*(\S+)", getsysinfo)
if not svc_tag:
logging.error(f"[!] Failed to retrieve SVC Tag for IP: {ip}")
return
# iDRAC MAC
idrac_mac = parse_single_value(r"MAC\s*Address\s*=\s*([0-9A-Fa-f:]{17})", getsysinfo)
# NIC.Integrated / Onboard MAC
integrated_1 = parse_single_value(r"NIC\.Integrated\.1-1-1\s+Ethernet\s*=\s*([0-9A-Fa-f:]{17})", getsysinfo)
integrated_2 = parse_single_value(r"NIC\.Integrated\.1-2-1\s+Ethernet\s*=\s*([0-9A-Fa-f:]{17})", getsysinfo)
onboard_1 = parse_single_value(r"NIC\.Embedded\.1-1-1\s+Ethernet\s*=\s*([0-9A-Fa-f:]{17})", getsysinfo)
onboard_2 = parse_single_value(r"NIC\.Embedded\.2-1-1\s+Ethernet\s*=\s*([0-9A-Fa-f:]{17})", getsysinfo)
# 메모리 / SSD 제조사
memory, ssd = extract_vendors(hwinventory)
# 결과 파일 저장
out_file = output_dir / f"{svc_tag}.txt"
with out_file.open("w", encoding="utf-8", newline="\n") as f:
f.write(f"{svc_tag}\n")
f.write(f"{integrated_1 or ''}\n")
f.write(f"{integrated_2 or ''}\n")
f.write(f"{onboard_1 or ''}\n")
f.write(f"{onboard_2 or ''}\n")
f.write(f"{idrac_mac or ''}\n")
f.write(f"{memory}\n")
f.write(f"{ssd}\n")
logging.info(f"[✔] {svc_tag} ({ip}) info saved.")
def main(ip_file: str) -> None:
ip_path = Path(ip_file)
if not ip_path.is_file():
logging.error(f"[!] IP file {ip_file} does not exist.")
return
output_dir = resolve_output_dir()
ips = [line.strip() for line in ip_path.read_text(encoding="utf-8").splitlines() if line.strip()]
if not ips:
logging.error("[!] No IP addresses found in the file.")
return
for ip in ips:
try:
fetch_idrac_info_one(ip, output_dir)
except Exception as e:
logging.error(f"[!] Error processing {ip}: {e}")
try:
ip_path.unlink(missing_ok=True)
except Exception:
pass
logging.info("\n정보 수집 완료.")
if __name__ == "__main__":
import sys, time
if len(sys.argv) != 2:
logging.error("Usage: python script.py <ip_file>")
sys.exit(1)
start = time.time()
main(sys.argv[1])
end = time.time()
elapsed = int(end - start)
h, m, s = elapsed // 3600, (elapsed % 3600) // 60, elapsed % 60
logging.info(f"수집 완료 시간: {h}시간 {m}{s}")

View File

@@ -6,37 +6,43 @@ import re
import subprocess
from pathlib import Path
from typing import Optional
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# ─────────────────────────────────────────────────────────────
# 저장 위치: 이 파일이 data/scripts/ 아래 있다면 → data/idrac_info
# 그 외 위치여도 이 파일의 상위 폴더에 idrac_info 생성
# 저장 위치 결정
def resolve_output_dir() -> Path:
here = Path(__file__).resolve().parent # 예: .../data/scripts
here = Path(__file__).resolve().parent
if here.name.lower() == "scripts" and here.parent.name.lower() == "data":
base = here.parent # data
base = here.parent
elif here.name.lower() == "scripts":
base = here.parent
else:
base = here.parent
out = base / "idrac_info"
out.mkdir(parents=True, exist_ok=True)
return out
# ─────────────────────────────────────────────────────────────
# 사용자 이름 및 비밀번호 (Bash 스크립트와 동일하게 하드코딩)
# iDRAC 계정
IDRAC_USER = "root"
IDRAC_PASS = "calvin"
def run(cmd: list[str]) -> str:
"""racadm 호출을 간단히 실행 (stdout만 수집)"""
"""racadm 명령 실행 (stdout만 반환)"""
try:
# join하여 getoutput로 호출 (Bash와 비슷한 동작)
return subprocess.getoutput(" ".join(cmd))
except Exception as e:
return f"" # 실패 시 빈 문자열
except Exception:
return ""
def parse_single_value(pattern: str, text: str) -> Optional[str]:
@@ -44,61 +50,81 @@ def parse_single_value(pattern: str, text: str) -> Optional[str]:
return m.group(1).strip() if m else None
def parse_mac_list_from_lines(text: str, fqdd_key: str) -> Optional[str]:
# ─────────────────────────────────────────────────────────────
# hwinventory 블록 기반 Manufacturer 추출 (핵심 수정 부분)
def extract_manufacturers(hwinventory: str, instance_prefix: str) -> list[str]:
"""
특정 FQDD 라인을 찾아 MAC 주소를 반환하는 간단한 도우미.
(Bash의 grep/awk 파이프를 정규표현식으로 대체)
instance_prefix:
- 'DIMM.' → 메모리
- 'Disk.Bay.' → 디스크
"""
# MAC 주소 패턴
mac_pat = r"([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})"
# 해당 FQDD 문자열이 포함된 줄과 가까운 곳에서 MAC을 찾는 간단한 방식
# (원 스크립트는 awk로 이전 줄을 보는 등 상세하지만 여기선 단순화)
block_pat = rf"{re.escape(fqdd_key)}.*?{mac_pat}"
m = re.search(block_pat, text, flags=re.IGNORECASE | re.DOTALL)
if m:
return m.group(1)
# 라인 전체에서 MAC만 스캔하는 fallback
m2 = re.search(mac_pat, text, flags=re.IGNORECASE)
return m2.group(1) if m2 else None
vendors = []
blocks = re.split(r"\n\s*\n", hwinventory)
for block in blocks:
if f"[InstanceID: {instance_prefix}" in block:
m = re.search(r"Manufacturer\s*=\s*(.+)", block, re.IGNORECASE)
if m:
vendors.append(m.group(1).strip())
return vendors
def fetch_idrac_info_one(ip: str, output_dir: Path) -> None:
# getsysinfo / hwinventory 호출
getsysinfo = run(["racadm", "-r", ip, "-u", IDRAC_USER, "-p", IDRAC_PASS, "getsysinfo"])
hwinventory = run(["racadm", "-r", ip, "-u", IDRAC_USER, "-p", IDRAC_PASS, "hwinventory"])
getsysinfo = run([
"racadm", "-r", ip,
"-u", IDRAC_USER,
"-p", IDRAC_PASS,
"getsysinfo"
])
# 서비스 태그
hwinventory = run([
"racadm", "-r", ip,
"-u", IDRAC_USER,
"-p", IDRAC_PASS,
"hwinventory"
])
# ── Service Tag
svc_tag = parse_single_value(r"SVC\s*Tag\s*=\s*(\S+)", getsysinfo)
if not svc_tag:
print(f"Failed to retrieve SVC Tag for IP: {ip}")
logging.error(f"[ERROR] SVC Tag 수집 실패: {ip}")
return
# iDRAC MAC
idrac_mac = parse_single_value(r"MAC Address\s*=\s*([0-9A-Fa-f:]{17})", getsysinfo)
# ── iDRAC MAC
idrac_mac = parse_single_value(
r"MAC Address\s*=\s*([0-9A-Fa-f:]{17})", getsysinfo
)
# NIC.Integrated MAC (1-1-1, 1-2-1)
integrated_1 = parse_single_value(r"NIC\.Integrated\.1-1-1.*?([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})",
getsysinfo)
integrated_2 = parse_single_value(r"NIC\.Integrated\.1-2-1.*?([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})",
getsysinfo)
# ── Integrated NIC
integrated_1 = parse_single_value(
r"NIC\.Integrated\.1-1-1.*?([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})",
getsysinfo
)
integrated_2 = parse_single_value(
r"NIC\.Integrated\.1-2-1.*?([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})",
getsysinfo
)
# Onboard MAC (Embedded 1-1-1, 2-1-1)
onboard_1 = parse_single_value(r"NIC\.Embedded\.1-1-1.*?([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})",
getsysinfo)
onboard_2 = parse_single_value(r"NIC\.Embedded\.2-1-1.*?([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})",
getsysinfo)
# ── Embedded NIC
onboard_1 = parse_single_value(
r"NIC\.Embedded\.1-1-1.*?([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})",
getsysinfo
)
onboard_2 = parse_single_value(
r"NIC\.Embedded\.2-1-1.*?([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})",
getsysinfo
)
# 벤더(메모리/SSD) 첫 글자 모음 (원 스크립트는 uniq+sort+cut -c1)
# 여기서는 Manufacturer= 값을 수집해 첫 글자만 취합 후 중복 제거.
mem_vendors = re.findall(r"DIMM.*?Manufacturer\s*=\s*(.+)", hwinventory, flags=re.IGNORECASE)
mem_vendors = [v.strip() for v in mem_vendors if v.strip()]
memory = "".join(sorted(set(v[0] for v in mem_vendors if v)))
# ─────────────────────────────────────────────
# Memory / Disk Vendor (수정 완료)
mem_vendors = sorted(set(extract_manufacturers(hwinventory, "DIMM.")))
ssd_vendors = sorted(set(extract_manufacturers(hwinventory, "Disk.Bay.")))
ssd_vendors = re.findall(r"Disk\.Bay.*?Manufacturer\s*=\s*(.+)", hwinventory, flags=re.IGNORECASE)
ssd_vendors = [v.strip() for v in ssd_vendors if v.strip()]
ssd = "".join(sorted(set(v[0] for v in ssd_vendors if v)))
memory = "\n".join(mem_vendors)
ssd = "\n".join(ssd_vendors)
# 파일 저장
# ── 결과 파일 저장
out_file = output_dir / f"{svc_tag}.txt"
with out_file.open("w", encoding="utf-8", newline="\n") as f:
f.write(f"{svc_tag}\n")
@@ -110,47 +136,53 @@ def fetch_idrac_info_one(ip: str, output_dir: Path) -> None:
f.write(f"{memory}\n")
f.write(f"{ssd}\n")
logging.info(f"[OK] {svc_tag} 수집 완료")
def main(ip_file: str) -> None:
ip_path = Path(ip_file)
if not ip_path.is_file():
print(f"IP file {ip_file} does not exist.")
logging.error(f"[ERROR] IP 파일 없음: {ip_file}")
return
output_dir = resolve_output_dir()
# Bash 스크립트는 파일 전체를 cat 하여 '하나의 IP'로 사용했지만,
# 여기서는 줄 단위로 모두 처리(한 줄만 있어도 동일하게 동작).
ips = [line.strip() for line in ip_path.read_text(encoding="utf-8").splitlines() if line.strip()]
ips = [
line.strip()
for line in ip_path.read_text(encoding="utf-8").splitlines()
if line.strip()
]
if not ips:
print("No IP addresses found in the file.")
logging.error("[ERROR] IP 목록이 비어 있습니다.")
return
# 순차 처리 (필요하면 ThreadPoolExecutor로 병렬화 가능)
for ip in ips:
try:
fetch_idrac_info_one(ip, output_dir)
except Exception as e:
print(f"Error processing {ip}: {e}")
logging.error(f"[ERROR] {ip} 처리 실패: {e}")
# 원본 Bash는 마지막에 입력 파일 삭제: rm -f $IP_FILE
# 원본 Bash 스크립트 동작 유지
try:
ip_path.unlink(missing_ok=True)
except Exception:
pass
print("정보 수집 완료.")
logging.info("정보 수집 완료.")
if __name__ == "__main__":
import sys, time
import sys
import time
if len(sys.argv) != 2:
print("Usage: python script.py <ip_file>")
logging.error("Usage: python script.py <ip_file>")
sys.exit(1)
start = time.time()
main(sys.argv[1])
end = time.time()
elapsed = int(end - start)
elapsed = int(time.time() - start)
h, m, s = elapsed // 3600, (elapsed % 3600) // 60, elapsed % 60
print(f"수집 완료 시간: {h} 시간, {m} 분, {s} 초.")
logging.info(f"수집 완료 시간: {h} 시간, {m} 분, {s} 초.")

View File

@@ -6,7 +6,14 @@ import re
import time
import subprocess
from pathlib import Path
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# ─────────────────────────────────────────────────────────────
# 저장 위치: 이 파일이 data/scripts/ 아래 있으면 → data/idrac_info
@@ -168,7 +175,7 @@ def fetch_idrac_info(ip: str, output_dir: Path) -> None:
r"Port")
if not svc_tag:
print(f"Failed to retrieve SVC Tag for IP: {ip}")
logging.error(f"Failed to retrieve SVC Tag for IP: {ip}")
return
out_file = output_dir / f"{svc_tag}.txt"
@@ -227,7 +234,7 @@ def fetch_idrac_info(ip: str, output_dir: Path) -> None:
def main(ip_file: str) -> None:
ip_path = Path(ip_file)
if not ip_path.is_file():
print(f"Usage: python script.py <ip_file>\nIP file {ip_file} does not exist.")
logging.error(f"Usage: python script.py <ip_file>\nIP file {ip_file} does not exist.")
return
output_dir = resolve_output_dir()
@@ -237,7 +244,7 @@ def main(ip_file: str) -> None:
lines = [ln.strip() for ln in ip_path.read_text(encoding="utf-8", errors="ignore").splitlines()]
ip = next((ln for ln in lines if ln), None)
if not ip:
print("No IP address found in the file.")
logging.error("No IP address found in the file.")
return
start = time.time()
@@ -252,13 +259,13 @@ def main(ip_file: str) -> None:
elapsed = int(end - start)
h, m, s = elapsed // 3600, (elapsed % 3600) // 60, elapsed % 60
print("정보 수집 완료.")
print(f"수집 완료 시간: {h} 시간, {m} 분, {s} 초.")
logging.info("정보 수집 완료.")
logging.info(f"수집 완료 시간: {h} 시간, {m} 분, {s} 초.")
if __name__ == "__main__":
import sys
if len(sys.argv) != 2:
print("Usage: python script.py <ip_file>")
logging.error("Usage: python script.py <ip_file>")
sys.exit(1)
main(sys.argv[1])

View File

@@ -1,316 +0,0 @@
#!/bin/bash
# 사용자 이름 및 비밀번호 설정
IDRAC_USER="root"
IDRAC_PASS="calvin"
# IP 주소 파일 경로 인자 받기
if [ -z "$1" ]; then
echo "Usage: $0 <ip_file>"
exit 1
fi
IP_FILE=$1
if [ ! -f "$IP_FILE" ]; then
echo "IP file $IP_FILE does not exist."
exit 1
fi
# 정보 저장 디렉터리 설정
OUTPUT_DIR="idrac_info"
mkdir -p $OUTPUT_DIR
# iDRAC 정보를 가져오는 함수 정의
fetch_idrac_info() {
local IDRAC_IP=$(cat $IP_FILE)
# 모든 hwinventory 저장
local hwinventory=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS hwinventory)
# 모든 샷시 정보 저장
local getsysinfo=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS getsysinfo)
# 모든 SysProfileSettings 저장
local SysProfileSettings=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get bios.SysProfileSettings)
# ProcessorSettings 저장
local ProcessorSettings=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get bios.ProcSettings)
# Memory Settings 저장
local MemorySettings=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get bios.MemSettings)
# Raid Settings 저장
local STORAGEController=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get STORAGE.Controller.1)
# 서비스 태그 가져오기
local SVC_TAG=$(echo "$getsysinfo" | grep -i "SVC Tag" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# Bios Firmware Version 확인
local Bios_firmware=$(echo "$getsysinfo" | grep -i "System BIOS Version" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# iDRAC Firmware Version 확인
local iDRAC_firmware=$(echo "$getsysinfo" | grep -i "Firmware Version" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# Intel NIC Firmware Version 확인
local Intel_NIC_firmware=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get NIC.FrmwImgMenu.1 | grep -i "#FamilyVersion" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# OnBoard NIC Firmware Version 확인
local Onboard_NIC_firmware=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get NIC.FrmwImgMenu.5 | grep -i "#FamilyVersion" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# R/C Firmware Version 확인
local Raid_firmware=$(echo "$hwinventory" | grep -i "ControllerFirmwareVersion" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# Bios 설정 Boot Mode 확인
local Bios_BootMode=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get bios.BiosBootSettings | grep -i "BootMode" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# Bios SysProfileSettings 설정 정보 확인
local SysProFileSettings_info1=$(echo "$SysProfileSettings" | grep -i "SysProfile=" | awk -F '=' '{print $2}' | tr -d '[:space:]')
local SysProFileSettings_info2=$(echo "$SysProfileSettings" | grep -i "ProcPwrPerf" | awk -F '=' '{print $2}' | tr -d '[:space:]')
local SysProFileSettings_info3=$(echo "$SysProfileSettings" | grep -i "MemFrequency" | awk -F '=' '{print $2}' | tr -d '[:space:]')
local SysProFileSettings_info4=$(echo "$SysProfileSettings" | grep -i "ProcTurboMode" | awk -F '=' '{print $2}' | tr -d '[:space:]')
local SysProFileSettings_info5=$(echo "$SysProfileSettings" | grep -i "PcieAspmL1" | awk -F '=' '{print $2}' | tr -d '[:space:]')
local SysProFileSettings_info6=$(echo "$SysProfileSettings" | grep -i "ProcCStates" | awk -F '=' '{print $2}' | tr -d '[:space:]')
local SysProFileSettings_info7=$(echo "$SysProfileSettings" | grep -i "DeterminismSlider" | awk -F '=' '{print $2}' | tr -d '[:space:]')
local SysProFileSettings_info8=$(echo "$SysProfileSettings" | grep -i "DynamicLinkWidthManagement" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# Processor Settings - Logical Processor
local ProcessorSettings_info1=$(echo "$ProcessorSettings" | grep -i "LogicalProc" | awk -F '=' '{print $2}' | tr -d '[:space:]')
local ProcessorSettings_info2=$(echo "$ProcessorSettings" | grep -i "ProcVirtualization" | awk -F '=' '{print $2}' | tr -d '[:space:]')
local ProcessorSettings_info3=$(echo "$ProcessorSettings" | grep -i "NumaNodesPerSocket" | awk -F '=' '{print $2}' | tr -d '[:space:]')
local ProcessorSettings_info4=$(echo "$ProcessorSettings" | grep -i "ProcX2Apic" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# Memory Settings - Node Interleaving
local MemorySettings_info1=$(echo "$MemorySettings" | grep -i "DramRefreshDelay" | awk -F '=' '{print $2}' | tr -d '[:space:]')
local MemorySettings_info2=$(echo "$MemorySettings" | grep -i "PPROnUCE" | awk -F '=' '{print $2}' | tr -d '[:space:]')
local MemorySettings_info3=$(echo "$MemorySettings" | grep -i "CECriticalSEL" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# System Settings - Thermal Profile Optimization
local SystemSettings_info1=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get System.ThermalSettings | grep -i "ThermalProfile" | awk -F '=' '{print $2}')
# Integrated Devices Settings - SR-IOV Global Enable
local IntegratedDevicesSettings_info1=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get Bios.IntegratedDevices | grep -i "SriovGlobalEnable" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# Miscellaneous Settings - F1/F2 Prompt on Error
local IMiscellaneousSettings_info1=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get bios.MiscSettings | grep -i "ErrPrompt" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# iDRAC Settings - Timezone
local iDRAC_Settings_info1=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get iDRAC.Time.Timezone | grep -i "Timezone" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# iDRAC Settings - IPMI LAN Selection
local iDRAC_Settings_info2=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get iDRAC.CurrentNIC | grep -i "ActiveNIC" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# iDRAC Settings - IPMI IP(IPv4)
local iDRAC_Settings_info3=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get iDRAC.CurrentIPv4 | grep -i "DHCPEnable" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# iDRAC Settings - IPMI IP(IPv6)
local iDRAC_Settings_info4=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get iDRAC.CurrentIPv6 | grep -i "Enable=" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# iDRAC Settings - Redfish Support
local iDRAC_Settings_info5=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get iDRAC.Redfish.Enable | grep -i "Enable=" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# iDRAC Settings - SSH Support
local iDRAC_Settings_info6=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get iDRAC.SSH | grep -i "Enable=" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# iDRAC Settings - AD User Domain Name
local iDRAC_Settings_info7=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get iDRAC.USERDomain.1.Name | grep -i "Name" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# iDRAC Settings - SC Server Address
local iDRAC_Settings_info8=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get iDRAC.ActiveDirectory.DomainController1 | grep -i "DomainController1" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# iDRAC Settings - SE AD RoleGroup Name
local iDRAC_Settings_info9=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get iDRAC.ADGroup.1.Name | grep -i "Name" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# iDRAC Settings - SE AD RoleGroup Dome인
local iDRAC_Settings_info10=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get iDRAC.ADGroup.1.Domain | grep -i "Domain" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# iDRAC Settings - SE AD RoleGroup Privilege
local iDRAC_Settings_info11=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get iDRAC.ADGroup.1.Privilege | grep -i "Privilege" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# iDRAC Settings - IDC AD RoleGroup name
local iDRAC_Settings_info12=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get iDRAC.ADGroup.2.Name | grep -i "Name" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# iDRAC Settings - IDC AD RoleGroup Dome인
local iDRAC_Settings_info13=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get iDRAC.ADGroup.2.Domain | grep -i "Domain" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# iDRAC Settings - IDC AD RoleGroup Privilege
local iDRAC_Settings_info14=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get iDRAC.ADGroup.2.Privilege | grep -i "Privilege" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# iDRAC Settings - Remote Log (syslog)
local iDRAC_Settings_info15=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get iDRAC.SysLog.SysLogEnable | grep -i "SysLogEnable" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# iDRAC Settings - syslog server address 1
local iDRAC_Settings_info16=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get iDRAC.SysLog.Server1 | grep -i "Server1" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# iDRAC Settings - syslog server address 2
local iDRAC_Settings_info17=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get iDRAC.SysLog.Server2 | grep -i "Server2" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# iDRAC Settings - syslog server port
local iDRAC_Settings_info18=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get iDRAC.SysLog.Port | grep -i "Port" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# iDRAC Settings - VirtualConsole Port
local iDRAC_Settings_info19=$(racadm -r $IDRAC_IP -u $IDRAC_USER -p $IDRAC_PASS get iDRAC.VirtualConsole.Port | grep -i "Port" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# RAID Settings - ProductName
local RAID_info0=$(echo "$hwinventory" | grep -i "ProductName = PERC" | awk -F '=' '{print $2}')
# RAID Settings - RAIDType
local RAID_info1=$(echo "$hwinventory" | grep -i "RAIDTypes" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# RAID Settings - StripeSize
local RAID_info2=$(echo "$hwinventory" | grep -i "StripeSize" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# RAID Settings - ReadCachePolicy
local RAID_info3=$(echo "$hwinventory" | grep -i "ReadCachePolicy" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# RAID Settings - WriteCachePolicy
local RAID_info4=$(echo "$hwinventory" | grep -i "WriteCachePolicy" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# RAID Settings - PatrolReadRate
local RAID_info5=$(echo "$STORAGEController" | grep -i "CheckConsistencyRate" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# RAID Settings - PatrolReadRate
local RAID_info6=$(echo "$STORAGEController" | grep -i "PatrolReadMode" | awk -F '=' '{print $2}' | tr -d '[:space:]')
# 서비스 태그가 존재하는지 확인
if [ -z "$SVC_TAG" ]; then
echo "Failed to retrieve SVC Tag for IP: $IDRAC_IP"
return
fi
local OUTPUT_FILE="$OUTPUT_DIR/$SVC_TAG.txt"
echo "Dell EMC Server Bios,iDRAC,R/C Setting (SVC Tag: $SVC_TAG)" | tee -a "$OUTPUT_FILE"
echo -e "\n" >> "$OUTPUT_FILE"
echo "------------------------------------------Firware Version 정보------------------------------------------" >> "$OUTPUT_FILE"
# SVC Tag 확인
echo "1. SVC Tag : $SVC_TAG" >> "$OUTPUT_FILE"
# Bios Firmware Version 확인
echo "2. Bios Firmware : $Bios_firmware" >> "$OUTPUT_FILE"
# iDRAC Firmware Version 확인
echo "3. iDRAC Firmware Version : $iDRAC_firmware" >> "$OUTPUT_FILE"
# Intel NIC Firmware Version 확인
echo "4. NIC Integrated Firmware Version : $Intel_NIC_firmware" >> "$OUTPUT_FILE"
# OnBoard NIC Firmware Version 확인
echo "5. OnBoard NIC Firmware Version : $Onboard_NIC_firmware" >> "$OUTPUT_FILE"
# Raid Controller Firmware Version 확인
echo "6. Raid Controller Firmware Version : $Raid_firmware" >> "$OUTPUT_FILE"
echo -e "\n" >> "$OUTPUT_FILE"
echo "---------------------------------------------Bios 설정 정보----------------------------------------------" >> "$OUTPUT_FILE"
# bios Boot Mode 확인
echo "01. Bios Boot Mode : $Bios_BootMode" >> "$OUTPUT_FILE"
# SysProfileSettings - System Profile
echo "02. System Profile Settings - System Profile : $SysProFileSettings_info1" >> "$OUTPUT_FILE"
# SysProfileSettings - CPU Power Management
echo "03. System Profile Settings - CPU Power Management : $SysProFileSettings_info2" >> "$OUTPUT_FILE"
# SysProfileSettings - Memory Frequency
echo "04. System Profile Settings - Memory Frequency : $SysProFileSettings_info3" >> "$OUTPUT_FILE"
# SysProfileSettings - Turbo Boost
echo "05. System Profile Settings - Turbo Boost : $SysProFileSettings_info4" >> "$OUTPUT_FILE"
# SysProfileSettings - C1E
echo "06. System Profile Settings - PCI ASPM L1 Link Power Management : $SysProFileSettings_info5" >> "$OUTPUT_FILE"
# SysProfileSettings - C-States
echo "07. System Profile Settings - C-States : $SysProFileSettings_info6" >> "$OUTPUT_FILE"
# SysProfileSettings - Determinism Slider
echo "08. System Profile Settings - Determinism Slider : $SysProFileSettings_info7" >> "$OUTPUT_FILE"
# SysProfileSettings - Dynamic Link Width Management (DLWM)
echo "08. System Profile Settings - Dynamic Link Width Management (DLWM) : $SysProFileSettings_info8" >> "$OUTPUT_FILE"
# Processor Settings - Logical Processor
echo "09. Processor Settings - Logical Processor : $ProcessorSettings_info1" >> "$OUTPUT_FILE"
# Processor Settings - Virtualization Technology
echo "10. Processor Settings - Virtualization Technology : $ProcessorSettings_info2" >> "$OUTPUT_FILE"
# Processor Settings - NUMA Nodes Per Socket
echo "11. Processor Settings - NUMA Nodes Per Socket : $ProcessorSettings_info3" >> "$OUTPUT_FILE"
# Processor Settings - x2APIC Mode
echo "12. Processor Settings - x2APIC Mode : $ProcessorSettings_info4" >> "$OUTPUT_FILE"
# Memory Settings - Dram Refresh Delayg
echo "13. Memory Settings - Dram Refresh Delay : $MemorySettings_info1" >> "$OUTPUT_FILE"
# Memory Settings - DIMM Self Healing (Post Package Repair) on Uncorrectable Memory Error
echo "14. Memory Settings - DIMM Self Healing (Post Package Repair) on Uncorrectable Memory Error : $MemorySettings_info2" >> "$OUTPUT_FILE"
# Memory Settings - Correctable Error Logging
echo "15. Memory Settings - Correctable Error Logging : $MemorySettings_info3" >> "$OUTPUT_FILE"
# System Settings - Thermal Profile Optimization
echo "16. System Settings - Thermal Profile Optimization : $SystemSettings_info1" >> "$OUTPUT_FILE"
# Integrated Devices Settings - SR-IOV Global Enable
echo "17. Integrated Devices Settings - SR-IOV Global Enable : $IntegratedDevicesSettings_info1" >> "$OUTPUT_FILE"
# Miscellaneous Settings - F1/F2 Prompt on Error
echo "18. Miscellaneous Settings - F1/F2 Prompt on Error : $IMiscellaneousSettings_info1" >> "$OUTPUT_FILE"
echo -e "\n" >> "$OUTPUT_FILE"
echo "---------------------------------------------iDRAC 설정 정보----------------------------------------------" >> "$OUTPUT_FILE"
# iDRAC Settings - Timezone
echo "01. iDRAC Settings - Timezone : $iDRAC_Settings_info1" >> "$OUTPUT_FILE"
# iDRAC Settings - IPMI LAN Selection
echo "02. iDRAC Settings - IPMI LAN Selection : $iDRAC_Settings_info2" >> "$OUTPUT_FILE"
# iDRAC Settings - IPMI IP(IPv4)
echo "03. iDRAC Settings - IPMI IP(IPv4) : $iDRAC_Settings_info3" >> "$OUTPUT_FILE"
# iDRAC Settings - IPMI IP(IPv6)
echo "04. iDRAC Settings - IPMI IP(IPv6) : $iDRAC_Settings_info4" >> "$OUTPUT_FILE"
# iDRAC Settings - Redfish Support
echo "05. iDRAC Settings - Redfish Support : $iDRAC_Settings_info5" >> "$OUTPUT_FILE"
# iDRAC Settings - SSH Support
echo "06. iDRAC Settings - SSH Support : $iDRAC_Settings_info6" >> "$OUTPUT_FILE"
# iDRAC Settings - AD User Domain Name
echo "07. iDRAC Settings - AD User Domain Name : $iDRAC_Settings_info7" >> "$OUTPUT_FILE"
# iDRAC Settings - SC Server Address
echo "08. iDRAC Settings - SC Server Address : $iDRAC_Settings_info8" >> "$OUTPUT_FILE"
# iDRAC Settings - SE AD RoleGroup Name
echo "09. iDRAC Settings - SE AD RoleGroup Name : $iDRAC_Settings_info9" >> "$OUTPUT_FILE"
# iDRAC Settings - SE AD RoleGroup Dome인
echo "10. iDRAC Settings - SE AD RoleGroup Dome인 : $iDRAC_Settings_info10" >> "$OUTPUT_FILE"
# iDRAC Settings - SE AD RoleGroup Privilege
echo "11. iDRAC Settings - SE AD RoleGroup Privilege : $iDRAC_Settings_info11" >> "$OUTPUT_FILE"
# iDRAC Settings - SE IDC RoleGroup Name
echo "12. iDRAC Settings - IDC AD RoleGroup Name : $iDRAC_Settings_info12" >> "$OUTPUT_FILE"
# iDRAC Settings - SE IDC RoleGroup Dome인
echo "13. iDRAC Settings - IDC AD RoleGroup Domain : $iDRAC_Settings_info13" >> "$OUTPUT_FILE"
# iDRAC Settings - SE IDC RoleGroup Dome인
echo "14. iDRAC Settings - IDC AD RoleGroup Privilege : $iDRAC_Settings_info14" >> "$OUTPUT_FILE"
# iDRAC Settings - Remote Log (syslog)
echo "15. iDRAC Settings - Remote Log (syslog) : $iDRAC_Settings_info15" >> "$OUTPUT_FILE"
# iDRAC Settings - Remote Log (syslog)
echo "16. iDRAC Settings - syslog server address 1 : $iDRAC_Settings_info16" >> "$OUTPUT_FILE"
# iDRAC Settings - Remote Log (syslog)
echo "17. iDRAC Settings - syslog server address 2 : $iDRAC_Settings_info17" >> "$OUTPUT_FILE"
# iDRAC Settings - syslog server port
echo "18. iDRAC Settings - syslog server port : $iDRAC_Settings_info18" >> "$OUTPUT_FILE"
# iDRAC Settings - Remote KVM Nonsecure port
echo "19. iDRAC Settings - Remote KVM Nonsecure port : $iDRAC_Settings_info19" >> "$OUTPUT_FILE"
echo -e "\n" >> "$OUTPUT_FILE"
# echo "---------------------------------------------Raid 설정 정보----------------------------------------------" >> "$OUTPUT_FILE"
# RAID Settings - Raid Types
#echo "01. RAID Settings - Raid ProductName : $RAID_info0" >> "$OUTPUT_FILE"
# RAID Settings - Raid Types
#echo "02. RAID Settings - Raid Typest : $RAID_info1" >> "$OUTPUT_FILE"
# RAID Settings - StripeSize
#echo "03. RAID Settings - StripeSize : $RAID_info2" >> "$OUTPUT_FILE"
# RAID Settings - ReadCachePolicy
#echo "04. RAID Settings - ReadCachePolicy : $RAID_info3" >> "$OUTPUT_FILE"
# RAID Settings - ReadCachePolicy
#echo "05. RAID Settings - WriteCachePolicy : $RAID_info4" >> "$OUTPUT_FILE"
# RAID Settings - CheckConsistencyRate
#echo "06. RAID Settings - CheckConsistencyRate : $RAID_info5" >> "$OUTPUT_FILE"
# RAID Settings - PatrolReadMode
#echo "07. RAID Settings - PatrolReadMode : $RAID_info6" >> "$OUTPUT_FILE"
# RAID Settings - period
#echo "08. RAID Settings - period : 168h" >> "$OUTPUT_FILE"
# RAID Settings - Power Save
#echo "09. RAID Settings - Power Save : No" >> "$OUTPUT_FILE"
# RAID Settings - JBODMODE
#echo "10. RAID Settings - JBODMODE : Controller does not support JBOD" >> "$OUTPUT_FILE"
# RAID Settings - maxconcurrentpd
#echo "11. RAID Settings - maxconcurrentpd : 240" >> "$OUTPUT_FILE"
# 임시 파일 삭제
rm -f $IP_FILE
}
export -f fetch_idrac_info
export IDRAC_USER
export IDRAC_PASS
export OUTPUT_DIR
# 시작 시간 기록
START_TIME=$(date +%s)
# IP 목록 파일을 읽어 병렬로 작업 수행
fetch_idrac_info
# 종료 시간 기록
END_TIME=$(date +%s)
# 소요 시간 계산
ELAPSED_TIME=$(($END_TIME - $START_TIME))
ELAPSED_HOURS=$(($ELAPSED_TIME / 3600))
ELAPSED_MINUTES=$((($ELAPSED_TIME % 3600) / 60))
ELAPSED_SECONDS=$(($ELAPSED_TIME % 60))
echo "정보 수집 완료."
echo "수집 완료 시간: $ELAPSED_HOURS 시간, $ELAPSED_MINUTES 분, $ELAPSED_SECONDS 초."

View File

@@ -10,6 +10,14 @@ import sys
import time
from pathlib import Path
from typing import Dict, Optional, Tuple, List
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# ===== 설정: 기본 계정/비밀번호 (환경변수로 덮어쓰기 가능) =====
IDRAC_USER = os.getenv("IDRAC_USER", "root")
@@ -190,17 +198,17 @@ def main():
ip_file = Path(args.ip_file)
if not ip_file.exists():
print(f"IP 파일이 존재하지 않습니다: {ip_file}", file=sys.stderr)
logging.error(f"IP 파일이 존재하지 않습니다: {ip_file}")
sys.exit(1)
with ip_file.open("r", encoding="utf-8") as f:
ips = [ln.strip() for ln in f if ln.strip()]
if not ips:
print("IP 목록이 비어 있습니다.", file=sys.stderr)
logging.error("IP 목록이 비어 있습니다.")
sys.exit(1)
print(f"[시작] 총 {len(ips)}대, workers={args.workers}, IDRAC_USER={IDRAC_USER}")
logging.info(f"[시작] 총 {len(ips)}대, workers={args.workers}, IDRAC_USER={IDRAC_USER}")
t0 = time.time()
ok = 0
@@ -214,20 +222,23 @@ def main():
try:
_ip, success, msg = fut.result()
prefix = "[OK] " if success else "[FAIL] "
print(prefix + ip + " - " + msg)
if success:
logging.info(prefix + ip + " - " + msg)
else:
logging.error(prefix + ip + " - " + msg)
ok += int(success)
fail += int(not success)
except Exception as e:
print(f"[EXC] {ip} - {e}", file=sys.stderr)
logging.error(f"[EXC] {ip} - {e}")
fail += 1
dt = int(time.time() - t0)
h, r = divmod(dt, 3600)
m, s = divmod(r, 60)
print("\n정보 수집 완료.")
print(f"성공 {ok}대 / 실패 {fail}")
print(f"수집 완료 시간: {h} 시간, {m} 분, {s} 초.")
logging.info("\n정보 수집 완료.")
logging.info(f"성공 {ok}대 / 실패 {fail}")
logging.info(f"수집 완료 시간: {h} 시간, {m} 분, {s} 초.")
if __name__ == "__main__":
main()

View File

@@ -10,6 +10,14 @@ import sys
import time
from pathlib import Path
from typing import Dict, Optional, Tuple, List
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# ===== 설정: 기본 계정/비밀번호 (환경변수로 덮어쓰기 가능) =====
IDRAC_USER = os.getenv("IDRAC_USER", "root")
@@ -189,17 +197,17 @@ def main():
ip_file = Path(args.ip_file)
if not ip_file.exists():
print(f"IP 파일이 존재하지 않습니다: {ip_file}", file=sys.stderr)
logging.error(f"IP 파일이 존재하지 않습니다: {ip_file}")
sys.exit(1)
with ip_file.open("r", encoding="utf-8") as f:
ips = [ln.strip() for ln in f if ln.strip()]
if not ips:
print("IP 목록이 비어 있습니다.", file=sys.stderr)
logging.error("IP 목록이 비어 있습니다.")
sys.exit(1)
print(f"[시작] 총 {len(ips)}대, workers={args.workers}, IDRAC_USER={IDRAC_USER}")
logging.info(f"[시작] 총 {len(ips)}대, workers={args.workers}, IDRAC_USER={IDRAC_USER}")
t0 = time.time()
ok = 0
@@ -213,20 +221,23 @@ def main():
try:
_ip, success, msg = fut.result()
prefix = "[OK] " if success else "[FAIL] "
print(prefix + ip + " - " + msg)
if success:
logging.info(prefix + ip + " - " + msg)
else:
logging.error(prefix + ip + " - " + msg)
ok += int(success)
fail += int(not success)
except Exception as e:
print(f"[EXC] {ip} - {e}", file=sys.stderr)
logging.error(f"[EXC] {ip} - {e}")
fail += 1
dt = int(time.time() - t0)
h, r = divmod(dt, 3600)
m, s = divmod(r, 60)
print("\n정보 수집 완료.")
print(f"성공 {ok}대 / 실패 {fail}")
print(f"수집 완료 시간: {h} 시간, {m} 분, {s} 초.")
logging.info("\n정보 수집 완료.")
logging.info(f"성공 {ok}대 / 실패 {fail}")
logging.info(f"수집 완료 시간: {h} 시간, {m} 분, {s} 초.")
if __name__ == "__main__":
main()

View File

@@ -1 +1 @@
10.10.0.1
10.10.0.2

29
idrac-info.service Normal file
View File

@@ -0,0 +1,29 @@
[Unit]
Description=iDRAC Info Web Service
After=network.target
[Service]
# 실행할 사용자 (보안을 위해 root 대신 전용 계정 권장, 예: idrac)
User=root
Group=root
# 프로젝트 루트 디렉토리 (서버 환경에 맞게 수정 필요)
WorkingDirectory=/data/app/idrac_info_new
# 가상환경의 python 실행 및 app.py 호출
# ExecStart=<python_path> <script_path>
ExecStart=/bin/bash -c '/data/app/idrac_info_new/venv/bin/python /data/app/idrac_info_new/app.py'
# 환경 변수 설정 (필요 시 수정)
Environment="FLASK_HOST=0.0.0.0"
Environment="FLASK_PORT=5000"
Environment="FLASK_DEBUG=false"
# Werkzeug 리로더 끄기 (프로덕션 모드) - Systemd에서는 이 설정이 오히려 오류(KeyError)를 유발하므로 제거
# Environment="WERKZEUG_RUN_MAIN=true"
# 프로세스 종료 시 자동 재시작
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

View File

@@ -129,20 +129,36 @@ def run_polling(flask_app: Flask) -> None:
if flask_app is None:
raise ValueError("flask_app is required for run_polling")
bot_token: Optional[str] = None
bot_name: Optional[str] = None
bot_id: Optional[int] = None
# DB에서 활성 봇 조회
with flask_app.app_context():
bots = TelegramBot.query.filter_by(is_active=True).all()
if not bots:
logger.warning("No active bots found")
logger.warning("No active bots found for polling service.")
return
# 첫 번째 활성 봇만 사용 (여러 봇이 동시에 폴링하면 충돌 가능)
if len(bots) > 1:
logger.warning("Multiple active bots found. Only the first one (%s) will be used.", bots[0].name)
# 첫 번째 활성 봇 사용
bot = bots[0]
flask_app.logger.info("Starting polling for bot: %s (ID: %s)", bot.name, bot.id)
# DB 세션 밖에서도 사용할 수 있도록 필요한 정보만 추출 (Detached Instance 에러 방지)
bot_token = bot.token
bot_name = bot.name
bot_id = bot.id
logger.info("Starting polling for bot: %s (ID: %s)", bot_name, bot_id)
if not bot_token:
logger.error("Bot token not found.")
return
# Application 생성
application = Application.builder().token(bot.token).build()
application = Application.builder().token(bot_token).build()
# Flask app을 bot_data에 넣어서 핸들러에서 사용할 수 있게 함
application.bot_data["flask_app"] = flask_app
@@ -152,6 +168,6 @@ def run_polling(flask_app: Flask) -> None:
try:
# v20 스타일: run_polling 은 동기 함수이고, 내부에서 이벤트 루프를 직접 관리함
application.run_polling(drop_pending_updates=True)
application.run_polling(drop_pending_updates=True, stop_signals=[])
except Exception as e:
flask_app.logger.exception("Error in bot polling: %s", e)
logger.exception("Error in bot polling: %s", e)