From f3965a67fdd85b308e3e360ac0a7e8bbcd85cfee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=9C=EA=B3=A8=EC=95=BD=EC=82=AC?= Date: Thu, 11 Sep 2025 12:07:53 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A8=B8=EC=8B=A0=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20Headscale=20CLI=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EC=99=84=EC=A0=84=20=EC=9E=AC=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - get_machine_detail 함수를 Headscale CLI 기반으로 완전 교체 - 기존 FARMQ DB 의존성에서 실시간 Headscale 데이터로 전환 - strftime 템플릿 오류 완전 해결 (datetime 객체 타입 체크 추가) - 실제 머신 정보 표시: 호스트명, IP 주소, 온라인 상태, 사용자 정보 - 약국 정보 매핑: Headscale 사용자명을 통한 약국 연동 - 시간 정보 인간화: "N시간 전", "N분 전" 형식으로 표시 - 네트워크 정보: IPv4/IPv6 주소, 엔드포인트, 키 정보 표시 - 조건부 모니터링 데이터 표시 (향후 확장 대비) - 전체 머신 상세 페이지 기능 정상화 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- farmq-admin/app.py | 28 ++++- farmq-admin/templates/machines/detail.html | 29 ++++- farmq-admin/utils/database_new.py | 119 ++++++++++++++------- 3 files changed, 136 insertions(+), 40 deletions(-) diff --git a/farmq-admin/app.py b/farmq-admin/app.py index 5ee94ff..683697f 100644 --- a/farmq-admin/app.py +++ b/farmq-admin/app.py @@ -117,7 +117,33 @@ def create_app(config_name=None): hostname = details.get('hostname', 'Unknown') print(f"✅ Rendering detail page for machine: {hostname}") - return render_template('machines/detail.html', machine=details) + # 템플릿에 필요한 추가 변수들 + is_online = details.get('online', False) + last_seen = details.get('last_seen') + + # 시간 형식화 + if last_seen: + from datetime import datetime, timezone + if isinstance(last_seen, datetime): + now = datetime.now(timezone.utc) if last_seen.tzinfo else datetime.now() + delta = now - last_seen + if delta.days > 0: + last_seen_humanized = f"{delta.days}일 전" + elif delta.seconds > 3600: + last_seen_humanized = f"{delta.seconds // 3600}시간 전" + elif delta.seconds > 60: + last_seen_humanized = f"{delta.seconds // 60}분 전" + else: + last_seen_humanized = "방금 전" + else: + last_seen_humanized = "알 수 없음" + else: + last_seen_humanized = "알 수 없음" + + return render_template('machines/detail.html', + machine=details, + is_online=is_online, + last_seen_humanized=last_seen_humanized) except Exception as e: print(f"❌ Error in machine_detail route: {e}") import traceback diff --git a/farmq-admin/templates/machines/detail.html b/farmq-admin/templates/machines/detail.html index 6719653..b47dbb7 100644 --- a/farmq-admin/templates/machines/detail.html +++ b/farmq-admin/templates/machines/detail.html @@ -96,7 +96,17 @@ 등록일 - {{ machine.created_at.strftime('%Y년 %m월 %d일 %H:%M') if machine.created_at else '알 수 없음' }} + + {% if machine.created_at %} + {% if machine.created_at.__class__.__name__ == 'datetime' %} + {{ machine.created_at.strftime('%Y년 %m월 %d일 %H:%M') }} + {% else %} + {{ machine.created_at }} + {% endif %} + {% else %} + 알 수 없음 + {% endif %} + @@ -141,7 +151,11 @@ 마지막 접속 {% if machine.last_seen %} - {{ machine.last_seen.strftime('%Y-%m-%d %H:%M:%S') }} + {% if machine.last_seen.__class__.__name__ == 'datetime' %} + {{ machine.last_seen.strftime('%Y-%m-%d %H:%M:%S') }} + {% else %} + {{ machine.last_seen }} + {% endif %}
{{ last_seen_humanized }} {% else %} 알 수 없음 @@ -210,7 +224,16 @@
현재 상태
- 최종 업데이트: {{ latest_monitoring.collected_at.strftime('%Y-%m-%d %H:%M:%S') }} + {% if latest_monitoring and latest_monitoring.collected_at %} + + 최종 업데이트: + {% if latest_monitoring.collected_at.__class__.__name__ == 'datetime' %} + {{ latest_monitoring.collected_at.strftime('%Y-%m-%d %H:%M:%S') }} + {% else %} + {{ latest_monitoring.collected_at }} + {% endif %} + + {% endif %}
diff --git a/farmq-admin/utils/database_new.py b/farmq-admin/utils/database_new.py index 7a705a0..a95ccbc 100644 --- a/farmq-admin/utils/database_new.py +++ b/farmq-admin/utils/database_new.py @@ -323,54 +323,101 @@ def get_all_machines_with_details() -> List[Dict[str, Any]]: close_session(headscale_session) def get_machine_detail(machine_id: int) -> Optional[Dict[str, Any]]: - """머신 상세 정보 조회""" - farmq_session = get_farmq_session() + """Headscale CLI를 통한 머신 상세 정보 조회""" + import subprocess + import json + from datetime import datetime try: - machine = farmq_session.query(MachineProfile).filter( - MachineProfile.id == machine_id - ).first() + # Headscale CLI에서 노드 목록 가져오기 + result = subprocess.run( + ['docker', 'exec', 'headscale', 'headscale', 'nodes', 'list', '-o', 'json'], + capture_output=True, + text=True, + check=True + ) + + nodes_data = json.loads(result.stdout) + + # machine_id로 노드 찾기 (id 기준) + machine = None + for node in nodes_data: + if node.get('id') == machine_id: + machine = node + break if not machine: return None - machine_data = machine.to_dict() + # 온라인 상태 체크 + online_status = get_headscale_online_status() + node_name = machine.get('given_name') or machine.get('name', '') + is_online = online_status.get(node_name.lower(), False) - # 약국 정보 - if machine.pharmacy_id: - pharmacy = farmq_session.query(PharmacyInfo).filter( - PharmacyInfo.id == machine.pharmacy_id - ).first() - if pharmacy: - machine_data['pharmacy'] = pharmacy.to_dict() + # 시간 변환 함수 + def convert_timestamp(ts_obj): + if isinstance(ts_obj, dict) and 'seconds' in ts_obj: + return datetime.fromtimestamp(ts_obj['seconds']) + elif isinstance(ts_obj, str): + try: + return datetime.fromisoformat(ts_obj.replace('Z', '+00:00')) + except: + return None + return None - # 최근 모니터링 데이터 (24시간) - cutoff_time = datetime.now() - timedelta(hours=24) - metrics = farmq_session.query(MonitoringMetrics).filter( - MonitoringMetrics.machine_profile_id == machine_id, - MonitoringMetrics.collected_at > cutoff_time - ).order_by(desc(MonitoringMetrics.collected_at)).limit(100).all() + # 엔드포인트 추출 + endpoints = [] + if 'endpoints' in machine: + endpoints = machine['endpoints'] - machine_data['metrics_history'] = [metric.to_dict() for metric in metrics] + # 약국 정보 매핑 (사용자명을 통해) + pharmacy_info = None + user_name = machine.get('user', {}).get('name', '') + if user_name: + farmq_session = get_farmq_session() + try: + pharmacy = farmq_session.query(PharmacyInfo).filter( + PharmacyInfo.headscale_user_name == user_name + ).first() + if pharmacy: + pharmacy_info = { + 'id': pharmacy.id, + 'name': pharmacy.pharmacy_name, + 'manager': pharmacy.manager_name, + 'address': pharmacy.address + } + finally: + farmq_session.close() - # 최신 메트릭스 - if metrics: - latest = metrics[0] - machine_data['latest_metrics'] = latest.to_dict() - machine_data['alerts'] = latest.get_alert_status() - - # 활성 알림들 - active_alerts = farmq_session.query(SystemAlert).filter( - SystemAlert.machine_profile_id == machine_id, - SystemAlert.status == 'active' - ).order_by(desc(SystemAlert.created_at)).limit(10).all() - - machine_data['active_alerts'] = [alert.to_dict() for alert in active_alerts] + # 반환 데이터 구성 + machine_data = { + 'id': machine.get('id'), + 'given_name': machine.get('given_name'), + 'hostname': machine.get('name'), + 'ipv4': machine.get('ip_addresses', [])[0] if machine.get('ip_addresses') else None, + 'ipv6': machine.get('ip_addresses', [])[1] if len(machine.get('ip_addresses', [])) > 1 else None, + 'machine_key': machine.get('machine_key'), + 'node_key': machine.get('node_key'), + 'disco_key': machine.get('disco_key'), + 'user': machine.get('user'), + 'last_seen': convert_timestamp(machine.get('last_seen')), + 'created_at': convert_timestamp(machine.get('created_at')), + 'register_method': 'CLI' if machine.get('register_method') == 1 else 'Pre-auth Key', + 'online': is_online, + 'endpoints': endpoints, + 'pharmacy': pharmacy_info, + # 헬퍼 메서드 + 'get_endpoints': lambda: endpoints, + } return machine_data - - finally: - close_session(farmq_session) + + except subprocess.CalledProcessError as e: + print(f"❌ Headscale CLI 오류: {e}") + return None + except Exception as e: + print(f"❌ 머신 상세 정보 조회 오류: {e}") + return None # ========================================== # Headscale Synchronization