first commit
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
"""Configuration generator for mGuard routers."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
import json
|
||||
|
||||
|
||||
@dataclass
|
||||
class GatewayConfig:
|
||||
"""Gateway configuration data."""
|
||||
name: str
|
||||
vpn_server: str
|
||||
vpn_port: int
|
||||
ca_cert: str
|
||||
client_cert: str
|
||||
client_key: str
|
||||
ta_key: Optional[str] = None
|
||||
|
||||
|
||||
class ConfigGenerator:
|
||||
"""Generate configuration files for mGuard routers."""
|
||||
|
||||
@staticmethod
|
||||
def generate_openvpn_config(config: GatewayConfig) -> str:
|
||||
"""Generate OpenVPN client configuration.
|
||||
|
||||
Args:
|
||||
config: Gateway configuration data
|
||||
|
||||
Returns:
|
||||
OpenVPN config file content
|
||||
"""
|
||||
ovpn = f"""# OpenVPN Client Configuration
|
||||
# Generated for: {config.name}
|
||||
|
||||
client
|
||||
dev tun
|
||||
proto udp
|
||||
remote {config.vpn_server} {config.vpn_port}
|
||||
resolv-retry infinite
|
||||
nobind
|
||||
persist-key
|
||||
persist-tun
|
||||
remote-cert-tls server
|
||||
cipher AES-256-GCM
|
||||
auth SHA256
|
||||
verb 3
|
||||
|
||||
<ca>
|
||||
{config.ca_cert}
|
||||
</ca>
|
||||
|
||||
<cert>
|
||||
{config.client_cert}
|
||||
</cert>
|
||||
|
||||
<key>
|
||||
{config.client_key}
|
||||
</key>
|
||||
"""
|
||||
|
||||
if config.ta_key:
|
||||
ovpn += f"""
|
||||
<tls-auth>
|
||||
{config.ta_key}
|
||||
</tls-auth>
|
||||
key-direction 1
|
||||
"""
|
||||
|
||||
return ovpn
|
||||
|
||||
@staticmethod
|
||||
def generate_atv_vpn_section(config: GatewayConfig) -> str:
|
||||
"""Generate ATV configuration section for VPN.
|
||||
|
||||
Note: This is a simplified version. Real ATV files have
|
||||
a more complex structure that should be merged with existing config.
|
||||
|
||||
Args:
|
||||
config: Gateway configuration data
|
||||
|
||||
Returns:
|
||||
ATV config section
|
||||
"""
|
||||
# ATV is essentially a key-value format
|
||||
# This is a simplified representation
|
||||
atv_section = f"""
|
||||
[vpn_client_1]
|
||||
enabled = 1
|
||||
name = {config.name}
|
||||
type = openvpn
|
||||
remote = {config.vpn_server}
|
||||
port = {config.vpn_port}
|
||||
protocol = udp
|
||||
cipher = AES-256-GCM
|
||||
"""
|
||||
return atv_section
|
||||
|
||||
@staticmethod
|
||||
def generate_mguard_script(
|
||||
gateway_name: str,
|
||||
endpoints: list[dict]
|
||||
) -> str:
|
||||
"""Generate mGuard CLI script for firewall configuration.
|
||||
|
||||
Args:
|
||||
gateway_name: Name of the gateway
|
||||
endpoints: List of endpoint configurations
|
||||
|
||||
Returns:
|
||||
Shell script for mGuard CLI
|
||||
"""
|
||||
script = f"""#!/bin/bash
|
||||
# Firewall configuration script for {gateway_name}
|
||||
# Run this on the mGuard via SSH
|
||||
|
||||
MBIN="/Packages/mguard-api_0/mbin"
|
||||
|
||||
echo "Configuring firewall rules for {gateway_name}..."
|
||||
|
||||
"""
|
||||
for i, ep in enumerate(endpoints):
|
||||
rule_name = f"endpoint_{i}_{ep['name'].replace(' ', '_')}"
|
||||
script += f"""
|
||||
# Rule for {ep['name']}
|
||||
$MBIN/action fwrules/add \\
|
||||
--name "{rule_name}" \\
|
||||
--source "any" \\
|
||||
--destination "{ep['internal_ip']}" \\
|
||||
--port "{ep['port']}" \\
|
||||
--protocol "{ep['protocol']}" \\
|
||||
--action "accept"
|
||||
"""
|
||||
|
||||
script += """
|
||||
echo "Firewall rules configured."
|
||||
$MBIN/action config/save
|
||||
echo "Configuration saved."
|
||||
"""
|
||||
return script
|
||||
|
||||
|
||||
def create_provisioning_package(
|
||||
gateway_name: str,
|
||||
vpn_server: str,
|
||||
vpn_port: int,
|
||||
ca_cert: str,
|
||||
client_cert: str,
|
||||
client_key: str,
|
||||
endpoints: list[dict],
|
||||
output_dir: str = "."
|
||||
) -> dict:
|
||||
"""Create a complete provisioning package.
|
||||
|
||||
Args:
|
||||
gateway_name: Name of the gateway
|
||||
vpn_server: VPN server address
|
||||
vpn_port: VPN server port
|
||||
ca_cert: CA certificate content
|
||||
client_cert: Client certificate content
|
||||
client_key: Client private key content
|
||||
endpoints: List of endpoint configurations
|
||||
output_dir: Output directory for files
|
||||
|
||||
Returns:
|
||||
Dictionary with file paths
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
output_path = Path(output_dir)
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
config = GatewayConfig(
|
||||
name=gateway_name,
|
||||
vpn_server=vpn_server,
|
||||
vpn_port=vpn_port,
|
||||
ca_cert=ca_cert,
|
||||
client_cert=client_cert,
|
||||
client_key=client_key
|
||||
)
|
||||
|
||||
# Generate OpenVPN config
|
||||
ovpn_content = ConfigGenerator.generate_openvpn_config(config)
|
||||
ovpn_file = output_path / f"{gateway_name}.ovpn"
|
||||
ovpn_file.write_text(ovpn_content)
|
||||
|
||||
# Generate firewall script
|
||||
fw_script = ConfigGenerator.generate_mguard_script(gateway_name, endpoints)
|
||||
fw_file = output_path / f"{gateway_name}_firewall.sh"
|
||||
fw_file.write_text(fw_script)
|
||||
|
||||
# Generate info JSON
|
||||
info = {
|
||||
"gateway_name": gateway_name,
|
||||
"vpn_server": vpn_server,
|
||||
"vpn_port": vpn_port,
|
||||
"endpoints": endpoints
|
||||
}
|
||||
info_file = output_path / f"{gateway_name}_info.json"
|
||||
info_file.write_text(json.dumps(info, indent=2))
|
||||
|
||||
return {
|
||||
"ovpn": str(ovpn_file),
|
||||
"firewall_script": str(fw_file),
|
||||
"info": str(info_file)
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
#!/usr/bin/env python3
|
||||
"""mGuard Provisioning Tool - CLI for gateway provisioning."""
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from mguard_api import MGuardAPIClient
|
||||
from config_generator import ConfigGenerator
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.option('--server', '-s', default='http://localhost:8000', help='API server URL')
|
||||
@click.option('--username', '-u', prompt=True, help='Admin username')
|
||||
@click.option('--password', '-p', prompt=True, hide_input=True, help='Admin password')
|
||||
@click.pass_context
|
||||
def cli(ctx, server, username, password):
|
||||
"""mGuard Gateway Provisioning Tool.
|
||||
|
||||
Use this tool to provision and configure mGuard routers.
|
||||
"""
|
||||
ctx.ensure_object(dict)
|
||||
|
||||
# Login to API
|
||||
import httpx
|
||||
client = httpx.Client(timeout=30.0)
|
||||
|
||||
try:
|
||||
response = client.post(
|
||||
f"{server}/api/auth/login",
|
||||
json={"username": username, "password": password}
|
||||
)
|
||||
response.raise_for_status()
|
||||
tokens = response.json()
|
||||
|
||||
ctx.obj['server'] = server
|
||||
ctx.obj['token'] = tokens['access_token']
|
||||
ctx.obj['client'] = client
|
||||
|
||||
console.print("[green]Successfully authenticated[/green]")
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
console.print(f"[red]Authentication failed: {e}[/red]")
|
||||
raise click.Abort()
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
def list_gateways(ctx):
|
||||
"""List all gateways."""
|
||||
client = ctx.obj['client']
|
||||
server = ctx.obj['server']
|
||||
token = ctx.obj['token']
|
||||
|
||||
response = client.get(
|
||||
f"{server}/api/gateways",
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
gateways = response.json()
|
||||
|
||||
table = Table(title="Gateways")
|
||||
table.add_column("ID", style="cyan")
|
||||
table.add_column("Name", style="green")
|
||||
table.add_column("Type")
|
||||
table.add_column("Status")
|
||||
table.add_column("Provisioned")
|
||||
|
||||
for gw in gateways:
|
||||
status = "[green]Online[/green]" if gw['is_online'] else "[red]Offline[/red]"
|
||||
prov = "[green]Yes[/green]" if gw['is_provisioned'] else "[yellow]No[/yellow]"
|
||||
table.add_row(
|
||||
str(gw['id']),
|
||||
gw['name'],
|
||||
gw['router_type'],
|
||||
status,
|
||||
prov
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument('gateway_id', type=int)
|
||||
@click.option('--output', '-o', default=None, help='Output file path')
|
||||
@click.pass_context
|
||||
def download_config(ctx, gateway_id, output):
|
||||
"""Download provisioning config for a gateway."""
|
||||
client = ctx.obj['client']
|
||||
server = ctx.obj['server']
|
||||
token = ctx.obj['token']
|
||||
|
||||
response = client.get(
|
||||
f"{server}/api/gateways/{gateway_id}/provision",
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
console.print(f"[red]Error: {response.text}[/red]")
|
||||
return
|
||||
|
||||
config = response.text
|
||||
|
||||
if output:
|
||||
with open(output, 'w') as f:
|
||||
f.write(config)
|
||||
console.print(f"[green]Config saved to {output}[/green]")
|
||||
else:
|
||||
output_file = f"gateway-{gateway_id}.ovpn"
|
||||
with open(output_file, 'w') as f:
|
||||
f.write(config)
|
||||
console.print(f"[green]Config saved to {output_file}[/green]")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument('gateway_id', type=int)
|
||||
@click.argument('router_ip')
|
||||
@click.option('--router-user', '-u', default='admin', help='Router username')
|
||||
@click.option('--router-pass', '-p', prompt=True, hide_input=True, help='Router password')
|
||||
@click.pass_context
|
||||
def provision_online(ctx, gateway_id, router_ip, router_user, router_pass):
|
||||
"""Provision a gateway via network (REST API or SSH).
|
||||
|
||||
GATEWAY_ID: ID of the gateway in the server database
|
||||
ROUTER_IP: IP address of the mGuard router
|
||||
"""
|
||||
client = ctx.obj['client']
|
||||
server = ctx.obj['server']
|
||||
token = ctx.obj['token']
|
||||
|
||||
console.print(f"[yellow]Connecting to router at {router_ip}...[/yellow]")
|
||||
|
||||
# Get gateway info
|
||||
response = client.get(
|
||||
f"{server}/api/gateways/{gateway_id}",
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
console.print(f"[red]Error: Gateway not found[/red]")
|
||||
return
|
||||
|
||||
gateway = response.json()
|
||||
firmware = gateway.get('firmware_version', '')
|
||||
|
||||
# Determine provisioning method
|
||||
if firmware and firmware.startswith('10.'):
|
||||
console.print("[cyan]Using REST API provisioning (Firmware 10.x)[/cyan]")
|
||||
_provision_rest_api(ctx, gateway, router_ip, router_user, router_pass)
|
||||
else:
|
||||
console.print("[cyan]Using SSH provisioning (Legacy firmware)[/cyan]")
|
||||
_provision_ssh(ctx, gateway, router_ip, router_user, router_pass)
|
||||
|
||||
|
||||
def _provision_rest_api(ctx, gateway, router_ip, router_user, router_pass):
|
||||
"""Provision via mGuard REST API."""
|
||||
mguard = MGuardAPIClient(router_ip, router_user, router_pass)
|
||||
|
||||
try:
|
||||
# Test connection
|
||||
if not mguard.test_connection():
|
||||
console.print("[red]Cannot connect to router REST API[/red]")
|
||||
return
|
||||
|
||||
# Download VPN config from server
|
||||
client = ctx.obj['client']
|
||||
server = ctx.obj['server']
|
||||
token = ctx.obj['token']
|
||||
|
||||
response = client.get(
|
||||
f"{server}/api/gateways/{gateway['id']}/provision",
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
vpn_config = response.text
|
||||
|
||||
# Apply VPN configuration
|
||||
console.print("[yellow]Applying VPN configuration...[/yellow]")
|
||||
if mguard.configure_vpn(vpn_config):
|
||||
console.print("[green]VPN configuration applied successfully![/green]")
|
||||
else:
|
||||
console.print("[red]Failed to apply VPN configuration[/red]")
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error: {e}[/red]")
|
||||
|
||||
|
||||
def _provision_ssh(ctx, gateway, router_ip, router_user, router_pass):
|
||||
"""Provision via SSH (legacy routers)."""
|
||||
import paramiko
|
||||
|
||||
try:
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(router_ip, username=router_user, password=router_pass, timeout=10)
|
||||
|
||||
console.print("[green]SSH connection established[/green]")
|
||||
|
||||
# Download VPN config from server
|
||||
client = ctx.obj['client']
|
||||
server = ctx.obj['server']
|
||||
token = ctx.obj['token']
|
||||
|
||||
response = client.get(
|
||||
f"{server}/api/gateways/{gateway['id']}/provision",
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
vpn_config = response.text
|
||||
|
||||
# Upload config file
|
||||
sftp = ssh.open_sftp()
|
||||
with sftp.file('/tmp/vpn.ovpn', 'w') as f:
|
||||
f.write(vpn_config)
|
||||
|
||||
console.print("[yellow]VPN config uploaded[/yellow]")
|
||||
|
||||
# Apply configuration (mGuard-specific commands)
|
||||
stdin, stdout, stderr = ssh.exec_command(
|
||||
'/Packages/mguard-api_0/mbin/action vpn/import /tmp/vpn.ovpn'
|
||||
)
|
||||
result = stdout.read().decode()
|
||||
|
||||
if 'error' in result.lower():
|
||||
console.print(f"[red]Error: {result}[/red]")
|
||||
else:
|
||||
console.print("[green]VPN configuration applied![/green]")
|
||||
|
||||
ssh.close()
|
||||
|
||||
except paramiko.AuthenticationException:
|
||||
console.print("[red]SSH authentication failed[/red]")
|
||||
except paramiko.SSHException as e:
|
||||
console.print(f"[red]SSH error: {e}[/red]")
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error: {e}[/red]")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument('config_file')
|
||||
@click.argument('router_ip')
|
||||
@click.option('--router-user', '-u', default='admin', help='Router username')
|
||||
@click.option('--router-pass', '-p', prompt=True, hide_input=True, help='Router password')
|
||||
def provision_offline(config_file, router_ip, router_user, router_pass):
|
||||
"""Provision a gateway using a downloaded config file.
|
||||
|
||||
CONFIG_FILE: Path to the .ovpn or .atv config file
|
||||
ROUTER_IP: IP address of the mGuard router (must be on same network)
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
config_path = Path(config_file)
|
||||
if not config_path.exists():
|
||||
console.print(f"[red]Config file not found: {config_file}[/red]")
|
||||
return
|
||||
|
||||
console.print(f"[yellow]Loading config from {config_file}...[/yellow]")
|
||||
config_content = config_path.read_text()
|
||||
|
||||
# Determine file type
|
||||
if config_file.endswith('.ovpn'):
|
||||
console.print("[cyan]OpenVPN config detected[/cyan]")
|
||||
elif config_file.endswith('.atv'):
|
||||
console.print("[cyan]mGuard ATV config detected[/cyan]")
|
||||
else:
|
||||
console.print("[yellow]Unknown config format, attempting generic upload[/yellow]")
|
||||
|
||||
# Try REST API first, then SSH
|
||||
mguard = MGuardAPIClient(router_ip, router_user, router_pass)
|
||||
|
||||
if mguard.test_connection():
|
||||
console.print("[cyan]Using REST API...[/cyan]")
|
||||
if mguard.upload_config(config_content):
|
||||
console.print("[green]Configuration uploaded successfully![/green]")
|
||||
else:
|
||||
console.print("[red]REST API upload failed[/red]")
|
||||
else:
|
||||
console.print("[cyan]REST API not available, trying SSH...[/cyan]")
|
||||
import paramiko
|
||||
|
||||
try:
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(router_ip, username=router_user, password=router_pass, timeout=10)
|
||||
|
||||
sftp = ssh.open_sftp()
|
||||
remote_path = f'/tmp/{config_path.name}'
|
||||
with sftp.file(remote_path, 'w') as f:
|
||||
f.write(config_content)
|
||||
|
||||
console.print(f"[green]Config uploaded to {remote_path}[/green]")
|
||||
|
||||
# Apply based on file type
|
||||
if config_file.endswith('.atv'):
|
||||
stdin, stdout, stderr = ssh.exec_command(
|
||||
f'/Packages/mguard-api_0/mbin/action config/restore {remote_path}'
|
||||
)
|
||||
else:
|
||||
stdin, stdout, stderr = ssh.exec_command(
|
||||
f'/Packages/mguard-api_0/mbin/action vpn/import {remote_path}'
|
||||
)
|
||||
|
||||
result = stdout.read().decode()
|
||||
console.print(f"Result: {result}")
|
||||
|
||||
ssh.close()
|
||||
console.print("[green]Provisioning complete![/green]")
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[red]SSH provisioning failed: {e}[/red]")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli(obj={})
|
||||
@@ -0,0 +1,175 @@
|
||||
"""mGuard REST API client for router configuration."""
|
||||
|
||||
import httpx
|
||||
from typing import Optional
|
||||
import urllib3
|
||||
|
||||
# Disable SSL warnings for self-signed certificates
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
|
||||
class MGuardAPIClient:
|
||||
"""Client for mGuard REST API (Firmware 10.x+)."""
|
||||
|
||||
def __init__(self, host: str, username: str, password: str, port: int = 443):
|
||||
"""Initialize mGuard API client.
|
||||
|
||||
Args:
|
||||
host: Router IP address or hostname
|
||||
username: Admin username
|
||||
password: Admin password
|
||||
port: HTTPS port (default 443)
|
||||
"""
|
||||
self.base_url = f"https://{host}:{port}"
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.client = httpx.Client(
|
||||
timeout=30.0,
|
||||
verify=False, # mGuard uses self-signed certs
|
||||
auth=(username, password)
|
||||
)
|
||||
|
||||
def test_connection(self) -> bool:
|
||||
"""Test connection to mGuard REST API."""
|
||||
try:
|
||||
response = self.client.get(f"{self.base_url}/api/v1/info")
|
||||
return response.status_code == 200
|
||||
except httpx.HTTPError:
|
||||
return False
|
||||
|
||||
def get_system_info(self) -> Optional[dict]:
|
||||
"""Get system information."""
|
||||
try:
|
||||
response = self.client.get(f"{self.base_url}/api/v1/info")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except httpx.HTTPError:
|
||||
return None
|
||||
|
||||
def get_configuration(self) -> Optional[dict]:
|
||||
"""Get current configuration."""
|
||||
try:
|
||||
response = self.client.get(f"{self.base_url}/api/v1/configuration")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except httpx.HTTPError:
|
||||
return None
|
||||
|
||||
def configure_vpn(self, vpn_config: str) -> bool:
|
||||
"""Configure OpenVPN client.
|
||||
|
||||
Args:
|
||||
vpn_config: OpenVPN configuration content
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
try:
|
||||
# The actual API endpoint depends on mGuard firmware version
|
||||
# This is an example - actual implementation may vary
|
||||
response = self.client.post(
|
||||
f"{self.base_url}/api/v1/vpn/openvpn/client",
|
||||
json={
|
||||
"enabled": True,
|
||||
"config": vpn_config
|
||||
}
|
||||
)
|
||||
return response.status_code in (200, 201, 204)
|
||||
except httpx.HTTPError:
|
||||
return False
|
||||
|
||||
def upload_config(self, config_content: str) -> bool:
|
||||
"""Upload configuration file.
|
||||
|
||||
Args:
|
||||
config_content: Configuration file content
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
try:
|
||||
response = self.client.post(
|
||||
f"{self.base_url}/api/v1/configuration",
|
||||
content=config_content,
|
||||
headers={"Content-Type": "application/octet-stream"}
|
||||
)
|
||||
return response.status_code in (200, 201, 204)
|
||||
except httpx.HTTPError:
|
||||
return False
|
||||
|
||||
def add_firewall_rule(
|
||||
self,
|
||||
name: str,
|
||||
source: str,
|
||||
destination: str,
|
||||
port: int,
|
||||
protocol: str = "tcp",
|
||||
action: str = "accept"
|
||||
) -> bool:
|
||||
"""Add a firewall rule.
|
||||
|
||||
Args:
|
||||
name: Rule name
|
||||
source: Source IP/network
|
||||
destination: Destination IP/network
|
||||
port: Destination port
|
||||
protocol: tcp or udp
|
||||
action: accept or drop
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
try:
|
||||
response = self.client.post(
|
||||
f"{self.base_url}/api/v1/firewall/rules",
|
||||
json={
|
||||
"name": name,
|
||||
"source": source,
|
||||
"destination": destination,
|
||||
"port": port,
|
||||
"protocol": protocol,
|
||||
"action": action,
|
||||
"enabled": True
|
||||
}
|
||||
)
|
||||
return response.status_code in (200, 201, 204)
|
||||
except httpx.HTTPError:
|
||||
return False
|
||||
|
||||
def remove_firewall_rule(self, rule_id: str) -> bool:
|
||||
"""Remove a firewall rule.
|
||||
|
||||
Args:
|
||||
rule_id: Rule ID to remove
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
try:
|
||||
response = self.client.delete(
|
||||
f"{self.base_url}/api/v1/firewall/rules/{rule_id}"
|
||||
)
|
||||
return response.status_code in (200, 204)
|
||||
except httpx.HTTPError:
|
||||
return False
|
||||
|
||||
def reboot(self) -> bool:
|
||||
"""Reboot the router."""
|
||||
try:
|
||||
response = self.client.post(f"{self.base_url}/api/v1/actions/reboot")
|
||||
return response.status_code in (200, 202, 204)
|
||||
except httpx.HTTPError:
|
||||
return False
|
||||
|
||||
def get_vpn_status(self) -> Optional[dict]:
|
||||
"""Get VPN connection status."""
|
||||
try:
|
||||
response = self.client.get(f"{self.base_url}/api/v1/vpn/status")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except httpx.HTTPError:
|
||||
return None
|
||||
|
||||
def close(self):
|
||||
"""Close the HTTP client."""
|
||||
self.client.close()
|
||||
@@ -0,0 +1,14 @@
|
||||
# HTTP Client
|
||||
httpx==0.26.0
|
||||
requests==2.31.0
|
||||
|
||||
# SSH for legacy routers
|
||||
paramiko==3.4.0
|
||||
|
||||
# CLI Interface
|
||||
click==8.1.7
|
||||
rich==13.7.0
|
||||
|
||||
# Configuration
|
||||
python-dotenv==1.0.0
|
||||
pyyaml==6.0.1
|
||||
Reference in New Issue
Block a user