@@ -315,5 +341,233 @@ function deleteMachine(machineId, machineName) {
showToast('ëšžì ìì ì€ ì€ë¥ê° ë°ìíìµëë€.', 'error');
});
}
+
+// 구ë
ì 볎 ë¡ë
+function loadSubscriptionInfo() {
+ const pharmacyId = {{ pharmacy.id }};
+
+ fetch(`/api/pharmacy/${pharmacyId}/subscriptions`)
+ .then(response => response.json())
+ .then(result => {
+ if (result.success) {
+ displaySubscriptionInfo(result.data);
+ } else {
+ showSubscriptionError();
+ }
+ })
+ .catch(error => {
+ console.error('구ë
ì 볎 ë¡ë ì€ë¥:', error);
+ showSubscriptionError();
+ });
+}
+
+function displaySubscriptionInfo(data) {
+ const activeSubscriptions = data.active_subscriptions;
+ const availableServices = data.available_services;
+
+ let html = '';
+
+ // íì± êµ¬ë
ìë¹ì€
+ if (activeSubscriptions.length > 0) {
+ html += `
+
+
+
구ë
ì€ìž ìë¹ì€
+
+
+
+ `;
+
+ let totalFee = 0;
+ activeSubscriptions.forEach(sub => {
+ totalFee += sub.monthly_fee;
+
+ const serviceIcons = {
+ 'CLOUD_PC': { icon: 'ð»', color: 'primary' },
+ 'AI_CCTV': { icon: 'ð·', color: 'info' },
+ 'CRM': { icon: 'ð', color: 'warning' }
+ };
+
+ const service = serviceIcons[sub.code] || { icon: 'ðŠ', color: 'secondary' };
+
+ html += `
+
+
+
+
+
${service.icon}
+
+
${sub.name}
+
+ ìììŒ: ${sub.start_date}
+ ë€ìê²°ì : ${sub.next_billing_date}
+
+
+
+
+
+ â©${sub.monthly_fee.toLocaleString()}/ì
+
+
+
+
+
+
+
+
+ `;
+ });
+
+ html += `
+
+
+
+
+ ìŽ ì 구ë
ë£: â©${totalFee.toLocaleString()}
+
+
+
+ `;
+ }
+
+ // 구ë
ê°ë¥í ìë¹ì€
+ if (availableServices.length > 0) {
+ html += `
+
+
+
구ë
ê°ë¥í ìë¹ì€
+
+
+
+ `;
+
+ availableServices.forEach(service => {
+ const serviceIcons = {
+ 'CLOUD_PC': { icon: 'ð»', color: 'primary' },
+ 'AI_CCTV': { icon: 'ð·', color: 'info' },
+ 'CRM': { icon: 'ð', color: 'warning' }
+ };
+
+ const serviceInfo = serviceIcons[service.code] || { icon: 'ðŠ', color: 'secondary' };
+
+ html += `
+
+
+
+
+
${serviceInfo.icon}
+
+
${service.name}
+
${service.description}
+
+
+
+
+ â©${service.monthly_price.toLocaleString()}/ì
+
+
+
+
+
+
+
+
+ `;
+ });
+
+ html += '
';
+ }
+
+ if (activeSubscriptions.length === 0 && availableServices.length === 0) {
+ html = `
+
+
+
구ë
ê°ë¥í ìë¹ì€ê° ììµëë€.
+
+ `;
+ }
+
+ document.getElementById('subscription-content').innerHTML = html;
+}
+
+function showSubscriptionError() {
+ document.getElementById('subscription-content').innerHTML = `
+
+
+
구ë
ì 볎륌 ë¶ë¬ì€ë ì€ ì€ë¥ê° ë°ìíìµëë€.
+
+
+ `;
+}
+
+function subscribeService(serviceId, serviceName, monthlyPrice) {
+ if (!confirm(`"${serviceName}" ìë¹ì€ë¥Œ ì â©${monthlyPrice.toLocaleString()}ì 구ë
íìê² ìµëê¹?`)) {
+ return;
+ }
+
+ const pharmacyId = {{ pharmacy.id }};
+
+ fetch('/api/subscriptions', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ pharmacy_id: pharmacyId,
+ product_id: serviceId,
+ monthly_fee: monthlyPrice
+ })
+ })
+ .then(response => response.json())
+ .then(result => {
+ if (result.success) {
+ showToast(result.message, 'success');
+ setTimeout(() => loadSubscriptionInfo(), 1000);
+ } else {
+ showToast(result.error, 'error');
+ }
+ })
+ .catch(error => {
+ console.error('구ë
ìì± ì€ë¥:', error);
+ showToast('구ë
ìì± ì€ ì€ë¥ê° ë°ìíìµëë€.', 'error');
+ });
+}
+
+function cancelSubscription(subscriptionId, serviceName) {
+ if (!confirm(`"${serviceName}" ìë¹ì€ 구ë
ì íŽì§íìê² ìµëê¹?`)) {
+ return;
+ }
+
+ fetch(`/api/subscriptions/${subscriptionId}`, {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ }
+ })
+ .then(response => response.json())
+ .then(result => {
+ if (result.success) {
+ showToast(result.message, 'success');
+ setTimeout(() => loadSubscriptionInfo(), 1000);
+ } else {
+ showToast(result.error, 'error');
+ }
+ })
+ .catch(error => {
+ console.error('구ë
íŽì§ ì€ë¥:', error);
+ showToast('구ë
íŽì§ ì€ ì€ë¥ê° ë°ìíìµëë€.', 'error');
+ });
+}
+
+// íìŽì§ ë¡ë ì 구ë
ì 볎 ë¡ë
+document.addEventListener('DOMContentLoaded', function() {
+ setTimeout(() => loadSubscriptionInfo(), 500);
+});
{% endblock %}
\ No newline at end of file
diff --git a/farmq-admin/templates/pharmacy/list.html b/farmq-admin/templates/pharmacy/list.html
index afe0672..a7427eb 100644
--- a/farmq-admin/templates/pharmacy/list.html
+++ b/farmq-admin/templates/pharmacy/list.html
@@ -40,6 +40,7 @@
| ìœêµ ì 볎 |
ëŽë¹ì |
+ 구ë
ìë¹ì€ |
ì°ê²°ë ëšžì |
ë€ížìí¬ ìí |
ì¡ì
|
@@ -68,6 +69,15 @@
{{ pharmacy_data.phone or 'ì°ëœì² 믞ë±ë¡' }}
+
+
+ |
{{ pharmacy_data.machine_count }}ë
@@ -200,6 +210,9 @@ let pharmacyModal;
document.addEventListener('DOMContentLoaded', function() {
pharmacyModal = new bootstrap.Modal(document.getElementById('pharmacyModal'));
+
+ // 구ë
ìí ë¡ë
+ setTimeout(loadSubscriptionStatuses, 500); // íìŽì§ ë¡ë í ìœê°ì ì§ì°
});
function showAddModal() {
@@ -316,6 +329,94 @@ document.getElementById('pharmacyForm').addEventListener('submit', function(e) {
}
});
+// 구ë
ìë¹ì€ ìí ë¡ë
+function loadSubscriptionStatuses() {
+ const subscriptionContainers = document.querySelectorAll('.subscription-services');
+
+ subscriptionContainers.forEach(container => {
+ const pharmacyId = container.dataset.pharmacyId;
+
+ fetch(`/api/pharmacy/${pharmacyId}/subscriptions`)
+ .then(response => response.json())
+ .then(result => {
+ if (result.success) {
+ displaySubscriptionStatus(container, result.data);
+ } else {
+ showSubscriptionError(container);
+ }
+ })
+ .catch(error => {
+ console.error(`ìœêµ ${pharmacyId} 구ë
ì 볎 ë¡ë ì€íš:`, error);
+ showSubscriptionError(container);
+ });
+ });
+}
+
+function displaySubscriptionStatus(container, data) {
+ const subscriptions = data.active_subscriptions;
+
+ if (subscriptions.length === 0) {
+ container.innerHTML = `
+
+
+ 구ë
ìì
+
+
+ `;
+ return;
+ }
+
+ // ìë¹ì€ ììŽìœ ë§µí
+ const serviceIcons = {
+ 'CLOUD_PC': { icon: 'ð»', color: 'primary', name: 'íŽëŒì°ëPC' },
+ 'AI_CCTV': { icon: 'ð·', color: 'info', name: 'AI CCTV' },
+ 'CRM': { icon: 'ð', color: 'warning', name: 'CRM' }
+ };
+
+ let html = ' ';
+ let totalFee = 0;
+
+ subscriptions.forEach(sub => {
+ const service = serviceIcons[sub.code] || { icon: 'ðŠ', color: 'secondary', name: sub.name };
+ totalFee += sub.monthly_fee;
+
+ html += `
+
+ ${service.icon}
+
+ `;
+ });
+
+ html += ' ';
+
+ // ìŽ ì 구ë
ë£ íì
+ html += `
+
+ â©${totalFee.toLocaleString()}/ì
+
+ `;
+
+ container.innerHTML = html;
+
+ // íŽí ìŽêž°í
+ const tooltipTriggerList = container.querySelectorAll('[data-bs-toggle="tooltip"]');
+ tooltipTriggerList.forEach(tooltipTriggerEl => {
+ new bootstrap.Tooltip(tooltipTriggerEl);
+ });
+}
+
+function showSubscriptionError(container) {
+ container.innerHTML = `
+
+
+ ì€ë¥
+
+
+ `;
+}
+
+// 구ë
ìí ë¡ë íšìë€ì ìì DOMContentLoadedìì ížì¶ëš
+
// í
ìŽëž ì ë ¬ ë° ê²ì êž°ë¥ ì¶ê° (í¥í)
{% endblock %}
\ No newline at end of file
diff --git a/fix-database-constraints.py b/fix-database-constraints.py
new file mode 100755
index 0000000..3d6f7bd
--- /dev/null
+++ b/fix-database-constraints.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env python3
+"""
+Headscale ë°ìŽí°ë² ìŽì€ ìžëí€ ì ìœì¡°ê±Ž ìì ì€í¬ëŠœíž
+"""
+
+import sqlite3
+import os
+from datetime import datetime
+
+def fix_database_constraints():
+ """ìžëí€ ì ìœì¡°ê±Ž 묞ì íŽê²°"""
+ db_path = '/srv/headscale-setup/data/db.sqlite'
+ backup_path = f'/srv/headscale-setup/data/db.sqlite.backup.{datetime.now().strftime("%Y%m%d_%H%M%S")}'
+
+ print("ð§ Headscale ë°ìŽí°ë² ìŽì€ ìžëí€ ì ìœì¡°ê±Ž ìì ")
+ print("=" * 60)
+
+ # 1. ë°±ì
ìì±
+ print(f"ðŠ ë°ìŽí°ë² ìŽì€ ë°±ì
ì€: {backup_path}")
+ import shutil
+ shutil.copy2(db_path, backup_path)
+ print("â
ë°±ì
ìë£")
+
+ # 2. ë°ìŽí°ë² ìŽì€ ì°ê²°
+ conn = sqlite3.connect(db_path)
+ cursor = conn.cursor()
+
+ try:
+ # 3. ìžëí€ ì ìœì¡°ê±Ž ë¹íì±í
+ print("ð ìžëí€ ì ìœì¡°ê±Ž ë¹íì±í ì€...")
+ cursor.execute("PRAGMA foreign_keys = OFF")
+
+ # 4. Ʞ졎 í
ìŽëžë€ íìž
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%pharmacy%'")
+ pharmacy_tables = cursor.fetchall()
+ print(f"ð ë°ê²¬ë ìœêµ êŽë š í
ìŽëž: {[table[0] for table in pharmacy_tables]}")
+
+ # 5. pharmacy_info í
ìŽëž ì¬ìì± (ìžëí€ ììŽ)
+ if any('pharmacy_info' in table[0] for table in pharmacy_tables):
+ print("ð pharmacy_info í
ìŽëž ì¬ìì± ì€...")
+
+ # Ʞ졎 ë°ìŽí° ë°±ì
+ cursor.execute("SELECT * FROM pharmacy_info")
+ existing_data = cursor.fetchall()
+
+ # Ʞ졎 í
ìŽëž 구조 íìž
+ cursor.execute("PRAGMA table_info(pharmacy_info)")
+ columns = cursor.fetchall()
+ print(f"ð Ʞ졎 컬ëŒ: {[col[1] for col in columns]}")
+
+ # ì í
ìŽëž ìì± (ìžëí€ ì ìœì¡°ê±Ž ìì)
+ cursor.execute("DROP TABLE IF EXISTS pharmacy_info_new")
+ cursor.execute("""
+ CREATE TABLE pharmacy_info_new (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ pharmacy_name VARCHAR(255) NOT NULL,
+ business_number VARCHAR(20),
+ manager_name VARCHAR(100),
+ phone VARCHAR(20),
+ address TEXT,
+ proxmox_host VARCHAR(255),
+ user_id VARCHAR(255), -- ìžëí€ ì ìœì¡°ê±Ž ì ê±°
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ """)
+
+ # Ʞ졎 ë°ìŽí° ë³µì¬
+ if existing_data:
+ print(f"ð Ʞ졎 ë°ìŽí° ë³µì¬ ì€... ({len(existing_data)}ê° ë ìœë)")
+ cursor.executemany("""
+ INSERT INTO pharmacy_info_new
+ (id, pharmacy_name, business_number, manager_name, phone, address, proxmox_host, user_id, created_at, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """, existing_data)
+
+ # Ʞ졎 í
ìŽëž ìì ë° ì í
ìŽëžë¡ êµì²Ž
+ cursor.execute("DROP TABLE pharmacy_info")
+ cursor.execute("ALTER TABLE pharmacy_info_new RENAME TO pharmacy_info")
+ print("â
pharmacy_info í
ìŽëž ì¬ìì± ìë£")
+
+ # 6. machine_specs í
ìŽëžë ìì (íìì)
+ if any('machine_specs' in table[0] for table in pharmacy_tables):
+ print("ð machine_specs í
ìŽëž íìž ì€...")
+
+ cursor.execute("PRAGMA table_info(machine_specs)")
+ columns = cursor.fetchall()
+
+ # ìžëí€ ì ìœì¡°ê±ŽìŽ ìëì§ íìž
+ cursor.execute("SELECT sql FROM sqlite_master WHERE name='machine_specs'")
+ table_sql = cursor.fetchone()
+
+ if table_sql and 'REFERENCES' in table_sql[0]:
+ print("ð machine_specs í
ìŽëž ì¬ìì± ì€...")
+
+ # Ʞ졎 ë°ìŽí° ë°±ì
+ cursor.execute("SELECT * FROM machine_specs")
+ existing_specs = cursor.fetchall()
+
+ # ì í
ìŽëž ìì± (ìžëí€ ì ìœì¡°ê±Ž ìì)
+ cursor.execute("DROP TABLE IF EXISTS machine_specs_new")
+ cursor.execute("""
+ CREATE TABLE machine_specs_new (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ machine_id INTEGER, -- ìžëí€ ì ìœì¡°ê±Ž ì ê±°
+ pharmacy_id INTEGER, -- ìžëí€ ì ìœì¡°ê±Ž ì ê±°
+ cpu_model VARCHAR(255),
+ cpu_cores INTEGER,
+ ram_gb INTEGER,
+ storage_gb INTEGER,
+ network_speed INTEGER,
+ os_info VARCHAR(255),
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ """)
+
+ # Ʞ졎 ë°ìŽí° ë³µì¬
+ if existing_specs:
+ print(f"ð Ʞ졎 ì€í ë°ìŽí° ë³µì¬ ì€... ({len(existing_specs)}ê° ë ìœë)")
+ cursor.executemany("""
+ INSERT INTO machine_specs_new
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """, existing_specs)
+
+ # Ʞ졎 í
ìŽëž ìì ë° ì í
ìŽëžë¡ êµì²Ž
+ cursor.execute("DROP TABLE machine_specs")
+ cursor.execute("ALTER TABLE machine_specs_new RENAME TO machine_specs")
+ print("â
machine_specs í
ìŽëž ì¬ìì± ìë£")
+
+ # 7. ë³ê²œì¬í 컀ë°
+ conn.commit()
+ print("ðŸ ë³ê²œì¬í ì ì¥ ìë£")
+
+ # 8. ìžëí€ ì ìœì¡°ê±Ž ì¬íì±í (íìì)
+ cursor.execute("PRAGMA foreign_keys = ON")
+
+ # 9. ë¬Žê²°ì± ê²ì¬
+ print("ð ë°ìŽí°ë² ìŽì€ ë¬Žê²°ì± ê²ì¬ ì€...")
+ cursor.execute("PRAGMA integrity_check")
+ integrity_result = cursor.fetchone()
+ print(f"â
ë¬Žê²°ì± ê²ì¬ 결곌: {integrity_result[0]}")
+
+ print("\nð ë°ìŽí°ë² ìŽì€ ìì ìë£!")
+ print(f"ðŠ ë°±ì
ìì¹: {backup_path}")
+ print("ð ìŽì Tailscale íŽëŒìŽìžíž ë±ë¡ì ë€ì ìëíŽë³Žìžì!")
+
+ except Exception as e:
+ print(f"â ì€ë¥ ë°ì: {e}")
+ conn.rollback()
+ print(f"ð ë°±ì
ìì ë³µìíë €ë©Ž: cp {backup_path} {db_path}")
+ raise
+ finally:
+ conn.close()
+
+if __name__ == '__main__':
+ fix_database_constraints()
\ No newline at end of file
diff --git a/quick-fix-db.py b/quick-fix-db.py
new file mode 100644
index 0000000..afdb35d
--- /dev/null
+++ b/quick-fix-db.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+"""
+ë¹ ë¥ž ë°ìŽí°ë² ìŽì€ ìì - ìžëí€ ì ìœì¡°ê±Žë§ ì ê±°
+"""
+
+import sqlite3
+import os
+from datetime import datetime
+
+def quick_fix():
+ """ë¹ ë¥Žê² ìžëí€ ì ìœì¡°ê±Žë§ ë¹íì±í"""
+ db_path = '/srv/headscale-setup/data/db.sqlite'
+
+ print("ð§ ë¹ ë¥ž ìì : ìžëí€ ì ìœì¡°ê±Ž ë¹íì±í")
+ print("=" * 50)
+
+ # Docker 컚í
ìŽë ì€ì§
+ print("â¹ïž Headscale 컚í
ìŽë ì€ì§ ì€...")
+ os.system("cd /srv/headscale-setup && docker-compose stop headscale")
+
+ # ë°ìŽí°ë² ìŽì€ ì°ê²°
+ conn = sqlite3.connect(db_path)
+ cursor = conn.cursor()
+
+ try:
+ # ìžëí€ ì ìœì¡°ê±Ž ì구 ë¹íì±í
+ print("ð ìžëí€ ì ìœì¡°ê±Ž ë¹íì±í...")
+ cursor.execute("PRAGMA foreign_keys = OFF")
+
+ # ì€ì íìž
+ cursor.execute("PRAGMA foreign_keys")
+ fk_status = cursor.fetchone()[0]
+ print(f"â
ìžëí€ ìí: {'ë¹íì±í' if fk_status == 0 else 'íì±í'}")
+
+ # í
ìŽëž íìž
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
+ tables = cursor.fetchall()
+ print(f"ð í
ìŽëž 목ë¡: {[table[0] for table in tables]}")
+
+ conn.commit()
+ print("ðŸ ì€ì ì ì¥ ìë£")
+
+ except Exception as e:
+ print(f"â ì€ë¥: {e}")
+ conn.rollback()
+ finally:
+ conn.close()
+
+ # Docker 컚í
ìŽë ì¬ìì
+ print("ð Headscale 컚í
ìŽë ì¬ìì ì€...")
+ os.system("cd /srv/headscale-setup && docker-compose start headscale")
+
+ print("â
ìë£! ìŽì Tailscale íŽëŒìŽìžíž ë±ë¡ì ë€ì ìëíŽë³Žìžì.")
+
+if __name__ == '__main__':
+ quick_fix()
\ No newline at end of file
diff --git a/test-proxmox-api.py b/test-proxmox-api.py
new file mode 100755
index 0000000..0b3be6a
--- /dev/null
+++ b/test-proxmox-api.py
@@ -0,0 +1,222 @@
+#!/usr/bin/env python3
+"""
+Proxmox VE API í
ì€íž ì€í¬ëŠœíž
+ì¬ì©ë²: python test-proxmox-api.py [root-password]
+"""
+
+import requests
+import json
+import sys
+import urllib3
+from urllib.parse import quote_plus
+
+# SSL ê²œê³ ë¬Žì
+urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+
+def test_api_token_method(host, token):
+ """API Token ë°©ì í
ì€íž"""
+ print("ð API Token ë°©ì í
ì€íž...")
+
+ headers = {
+ 'Authorization': f'PVEAPIToken={token}'
+ }
+
+ try:
+ response = requests.get(
+ f'https://{host}:443/api2/json/version',
+ headers=headers,
+ verify=False,
+ timeout=10
+ )
+
+ if response.status_code == 200:
+ version_info = response.json()['data']
+ print(f"â
API Token ìžìŠ ì±ê³µ!")
+ print(f" Proxmox Version: {version_info['version']}")
+ print(f" Release: {version_info['release']}")
+ return True
+ else:
+ print(f"â API Token ìžìŠ ì€íš: {response.status_code}")
+ print(f" ìëµ: {response.text}")
+ return False
+
+ except Exception as e:
+ print(f"â API Token ì°ê²° ì€íš: {e}")
+ return False
+
+def test_session_method(host, username, password):
+ """ìžì
ì¿ í€ ë°©ì í
ì€íž"""
+ print("ðª ìžì
ì¿ í€ ë°©ì í
ì€íž...")
+
+ # 1ëšê³: ë¡ê·žìžíì¬ í°ìŒ íë
+ login_data = {
+ 'username': username,
+ 'password': password
+ }
+
+ try:
+ response = requests.post(
+ f'https://{host}:443/api2/json/access/ticket',
+ data=login_data,
+ verify=False,
+ timeout=10
+ )
+
+ if response.status_code != 200:
+ print(f"â ë¡ê·žìž ì€íš: {response.status_code}")
+ print(f" ìëµ: {response.text}")
+ return False, None, None
+
+ data = response.json()['data']
+ ticket = data['ticket']
+ csrf_token = data['CSRFPreventionToken']
+
+ print("â
ë¡ê·žìž ì±ê³µ!")
+ print(f" í°ìŒ: {ticket[:20]}...")
+ print(f" CSRF: {csrf_token}")
+
+ return True, ticket, csrf_token
+
+ except Exception as e:
+ print(f"â ë¡ê·žìž ì°ê²° ì€íš: {e}")
+ return False, None, None
+
+def test_vm_list_api(host, ticket=None, csrf_token=None, api_token=None):
+ """VM ëª©ë¡ ì¡°í API í
ì€íž"""
+ print("\nð VM ëª©ë¡ ì¡°í API í
ì€íž...")
+
+ if api_token:
+ headers = {'Authorization': f'PVEAPIToken={api_token}'}
+ cookies = None
+ else:
+ headers = {'CSRFPreventionToken': csrf_token}
+ cookies = {'PVEAuthCookie': ticket}
+
+ try:
+ response = requests.get(
+ f'https://{host}:443/api2/json/cluster/resources?type=vm',
+ headers=headers,
+ cookies=cookies,
+ verify=False,
+ timeout=10
+ )
+
+ if response.status_code == 200:
+ vms = response.json()['data']
+ print(f"â
VM ëª©ë¡ ì¡°í ì±ê³µ! (ìŽ {len(vms)}ê°)")
+
+ for vm in vms:
+ status_icon = "ð¢" if vm.get('status') == 'running' else "ðŽ"
+ print(f" {status_icon} VM {vm.get('vmid')}: {vm.get('name', 'N/A')} ({vm.get('status', 'unknown')})")
+
+ return True, vms
+ else:
+ print(f"â VM ëª©ë¡ ì¡°í ì€íš: {response.status_code}")
+ print(f" ìëµ: {response.text}")
+ return False, []
+
+ except Exception as e:
+ print(f"â VM ëª©ë¡ API ì°ê²° ì€íš: {e}")
+ return False, []
+
+def test_vnc_proxy_api(host, node, vmid, ticket=None, csrf_token=None, api_token=None):
+ """VNC íë¡ì í°ìŒ ìì± í
ì€íž"""
+ print(f"\nð¥ïž VNC íë¡ì API í
ì€íž (VM {vmid})...")
+
+ if api_token:
+ headers = {'Authorization': f'PVEAPIToken={api_token}'}
+ cookies = None
+ else:
+ headers = {'CSRFPreventionToken': csrf_token}
+ cookies = {'PVEAuthCookie': ticket}
+
+ data = {'websocket': '1'}
+
+ try:
+ response = requests.post(
+ f'https://{host}:443/api2/json/nodes/{node}/qemu/{vmid}/vncproxy',
+ headers=headers,
+ cookies=cookies,
+ data=data,
+ verify=False,
+ timeout=10
+ )
+
+ if response.status_code == 200:
+ vnc_data = response.json()['data']
+ print("â
VNC íë¡ì í°ìŒ ìì± ì±ê³µ!")
+ print(f" í¬íž: {vnc_data['port']}")
+ print(f" í°ìŒ: {vnc_data['ticket'][:20]}...")
+
+ # WebSocket URL ìì±
+ encoded_ticket = quote_plus(vnc_data['ticket'])
+ ws_url = f"wss://{host}:443/api2/json/nodes/{node}/qemu/{vmid}/vncwebsocket?port={vnc_data['port']}&vncticket={encoded_ticket}"
+ print(f" WebSocket URL: {ws_url[:80]}...")
+
+ return True, vnc_data
+ else:
+ print(f"â VNC íë¡ì ìì± ì€íš: {response.status_code}")
+ print(f" ìëµ: {response.text}")
+ return False, None
+
+ except Exception as e:
+ print(f"â VNC íë¡ì API ì°ê²° ì€íš: {e}")
+ return False, None
+
+def main():
+ if len(sys.argv) < 2:
+ print("ì¬ì©ë²: python test-proxmox-api.py [root-password] [api-token]")
+ print()
+ print("ìì:")
+ print(" python test-proxmox-api.py 100.64.0.1 mypassword")
+ print(" python test-proxmox-api.py 100.64.0.1 '' root@pam!token=uuid")
+ sys.exit(1)
+
+ host = sys.argv[1]
+ password = sys.argv[2] if len(sys.argv) > 2 else None
+ api_token = sys.argv[3] if len(sys.argv) > 3 else None
+
+ print("=" * 60)
+ print(f"ð Proxmox API í
ì€íž - {host}")
+ print("=" * 60)
+
+ # API Token ë°©ì í
ì€íž
+ if api_token:
+ success = test_api_token_method(host, api_token)
+ if success:
+ # VM ëª©ë¡ í
ì€íž
+ vm_success, vms = test_vm_list_api(host, api_token=api_token)
+
+ # ì€í ì€ìž VMìŽ ììŒë©Ž VNC í
ì€íž
+ if vm_success and vms:
+ running_vm = next((vm for vm in vms if vm.get('status') == 'running'), None)
+ if running_vm:
+ test_vnc_proxy_api(host, running_vm['node'], running_vm['vmid'], api_token=api_token)
+ else:
+ print("â ïž ì€í ì€ìž VMìŽ ììŽ VNC í
ì€ížë¥Œ 걎ëëëë€.")
+
+ # íšì€ìë ë°©ì í
ì€íž
+ elif password:
+ login_success, ticket, csrf_token = test_session_method(host, 'root@pam', password)
+
+ if login_success:
+ # VM ëª©ë¡ í
ì€íž
+ vm_success, vms = test_vm_list_api(host, ticket, csrf_token)
+
+ # ì€í ì€ìž VMìŽ ììŒë©Ž VNC í
ì€íž
+ if vm_success and vms:
+ running_vm = next((vm for vm in vms if vm.get('status') == 'running'), None)
+ if running_vm:
+ test_vnc_proxy_api(host, running_vm['node'], running_vm['vmid'], ticket, csrf_token)
+ else:
+ print("â ïž ì€í ì€ìž VMìŽ ììŽ VNC í
ì€ížë¥Œ 걎ëëëë€.")
+
+ else:
+ print("â íšì€ìë ëë API í í°ì ì ê³µíŽì£Œìžì.")
+ sys.exit(1)
+
+ print("\n" + "=" * 60)
+ print("â
í
ì€íž ìë£!")
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
|