- pair_devices.html template for device pairing interface - SLMM device control lock prevents flooding nl43. Fix: - Polling intervals for SLMM. - modem view now list - device pairing much improved. - various other tweaks through out UI. - SLMM Scheduled downloads fixed.
232 lines
7.1 KiB
Python
232 lines
7.1 KiB
Python
"""
|
|
SLMM Synchronization Service
|
|
|
|
This service ensures Terra-View roster is the single source of truth for SLM device configuration.
|
|
When SLM devices are added, edited, or deleted in Terra-View, changes are automatically synced to SLMM.
|
|
"""
|
|
|
|
import logging
|
|
import httpx
|
|
import os
|
|
from typing import Optional
|
|
from sqlalchemy.orm import Session
|
|
|
|
from backend.models import RosterUnit
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SLMM_BASE_URL = os.getenv("SLMM_BASE_URL", "http://localhost:8100")
|
|
|
|
|
|
async def sync_slm_to_slmm(unit: RosterUnit) -> bool:
|
|
"""
|
|
Sync a single SLM device from Terra-View roster to SLMM.
|
|
|
|
Args:
|
|
unit: RosterUnit with device_type="slm"
|
|
|
|
Returns:
|
|
True if sync successful, False otherwise
|
|
"""
|
|
if unit.device_type != "slm":
|
|
logger.warning(f"Attempted to sync non-SLM unit {unit.id} to SLMM")
|
|
return False
|
|
|
|
if not unit.slm_host:
|
|
logger.warning(f"SLM {unit.id} has no host configured, skipping SLMM sync")
|
|
return False
|
|
|
|
# Disable polling if unit is benched (deployed=False) or retired
|
|
# Only actively deployed units should be polled
|
|
should_poll = unit.deployed and not unit.retired
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
response = await client.put(
|
|
f"{SLMM_BASE_URL}/api/nl43/{unit.id}/config",
|
|
json={
|
|
"host": unit.slm_host,
|
|
"tcp_port": unit.slm_tcp_port or 2255,
|
|
"tcp_enabled": True,
|
|
"ftp_enabled": True,
|
|
"ftp_username": "USER", # Default NL43 credentials
|
|
"ftp_password": "0000",
|
|
"poll_enabled": should_poll, # Disable polling for benched or retired units
|
|
"poll_interval_seconds": 3600, # Default to 1 hour polling
|
|
}
|
|
)
|
|
|
|
if response.status_code in [200, 201]:
|
|
logger.info(f"✓ Synced SLM {unit.id} to SLMM at {unit.slm_host}:{unit.slm_tcp_port or 2255}")
|
|
return True
|
|
else:
|
|
logger.error(f"Failed to sync SLM {unit.id} to SLMM: {response.status_code} {response.text}")
|
|
return False
|
|
|
|
except httpx.TimeoutException:
|
|
logger.error(f"Timeout syncing SLM {unit.id} to SLMM")
|
|
return False
|
|
except Exception as e:
|
|
logger.error(f"Error syncing SLM {unit.id} to SLMM: {e}")
|
|
return False
|
|
|
|
|
|
async def delete_slm_from_slmm(unit_id: str) -> bool:
|
|
"""
|
|
Delete a device from SLMM database.
|
|
|
|
Args:
|
|
unit_id: The unit ID to delete
|
|
|
|
Returns:
|
|
True if deletion successful or device doesn't exist, False on error
|
|
"""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
response = await client.delete(
|
|
f"{SLMM_BASE_URL}/api/nl43/{unit_id}/config"
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
logger.info(f"✓ Deleted SLM {unit_id} from SLMM")
|
|
return True
|
|
elif response.status_code == 404:
|
|
logger.info(f"SLM {unit_id} not found in SLMM (already deleted)")
|
|
return True
|
|
else:
|
|
logger.error(f"Failed to delete SLM {unit_id} from SLMM: {response.status_code} {response.text}")
|
|
return False
|
|
|
|
except httpx.TimeoutException:
|
|
logger.error(f"Timeout deleting SLM {unit_id} from SLMM")
|
|
return False
|
|
except Exception as e:
|
|
logger.error(f"Error deleting SLM {unit_id} from SLMM: {e}")
|
|
return False
|
|
|
|
|
|
async def sync_all_slms_to_slmm(db: Session) -> dict:
|
|
"""
|
|
Sync all SLM devices from Terra-View roster to SLMM.
|
|
|
|
This ensures SLMM database matches Terra-View roster as the source of truth.
|
|
Should be called on Terra-View startup and optionally via admin endpoint.
|
|
|
|
Args:
|
|
db: Database session
|
|
|
|
Returns:
|
|
Dictionary with sync results
|
|
"""
|
|
logger.info("Starting full SLM sync to SLMM...")
|
|
|
|
# Get all SLM units from roster
|
|
slm_units = db.query(RosterUnit).filter_by(device_type="slm").all()
|
|
|
|
results = {
|
|
"total": len(slm_units),
|
|
"synced": 0,
|
|
"skipped": 0,
|
|
"failed": 0
|
|
}
|
|
|
|
for unit in slm_units:
|
|
# Skip units without host configured
|
|
if not unit.slm_host:
|
|
results["skipped"] += 1
|
|
logger.debug(f"Skipped {unit.unit_type} - no host configured")
|
|
continue
|
|
|
|
# Sync to SLMM
|
|
success = await sync_slm_to_slmm(unit)
|
|
if success:
|
|
results["synced"] += 1
|
|
else:
|
|
results["failed"] += 1
|
|
|
|
logger.info(
|
|
f"SLM sync complete: {results['synced']} synced, "
|
|
f"{results['skipped']} skipped, {results['failed']} failed"
|
|
)
|
|
|
|
return results
|
|
|
|
|
|
async def get_slmm_devices() -> Optional[list]:
|
|
"""
|
|
Get list of all devices currently in SLMM database.
|
|
|
|
Returns:
|
|
List of device unit_ids, or None on error
|
|
"""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
response = await client.get(f"{SLMM_BASE_URL}/api/nl43/_polling/status")
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
return [device["unit_id"] for device in data["data"]["devices"]]
|
|
else:
|
|
logger.error(f"Failed to get SLMM devices: {response.status_code}")
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting SLMM devices: {e}")
|
|
return None
|
|
|
|
|
|
async def cleanup_orphaned_slmm_devices(db: Session) -> dict:
|
|
"""
|
|
Remove devices from SLMM that are not in Terra-View roster.
|
|
|
|
This cleans up orphaned test devices or devices that were manually added to SLMM.
|
|
|
|
Args:
|
|
db: Database session
|
|
|
|
Returns:
|
|
Dictionary with cleanup results
|
|
"""
|
|
logger.info("Checking for orphaned devices in SLMM...")
|
|
|
|
# Get all device IDs from SLMM
|
|
slmm_devices = await get_slmm_devices()
|
|
if slmm_devices is None:
|
|
return {"error": "Failed to get SLMM device list"}
|
|
|
|
# Get all SLM unit IDs from Terra-View roster
|
|
roster_units = db.query(RosterUnit.id).filter_by(device_type="slm").all()
|
|
roster_unit_ids = {unit.id for unit in roster_units}
|
|
|
|
# Find orphaned devices (in SLMM but not in roster)
|
|
orphaned = [uid for uid in slmm_devices if uid not in roster_unit_ids]
|
|
|
|
results = {
|
|
"total_in_slmm": len(slmm_devices),
|
|
"total_in_roster": len(roster_unit_ids),
|
|
"orphaned": len(orphaned),
|
|
"deleted": 0,
|
|
"failed": 0,
|
|
"orphaned_devices": orphaned
|
|
}
|
|
|
|
if not orphaned:
|
|
logger.info("No orphaned devices found in SLMM")
|
|
return results
|
|
|
|
logger.info(f"Found {len(orphaned)} orphaned devices in SLMM: {orphaned}")
|
|
|
|
# Delete orphaned devices
|
|
for unit_id in orphaned:
|
|
success = await delete_slm_from_slmm(unit_id)
|
|
if success:
|
|
results["deleted"] += 1
|
|
else:
|
|
results["failed"] += 1
|
|
|
|
logger.info(
|
|
f"Cleanup complete: {results['deleted']} deleted, {results['failed']} failed"
|
|
)
|
|
|
|
return results
|