Separation of performing subprocess commands, parsing them for error strings, and handling those errors as enum types. Systemwide error handling was introduced via a wrapper to all systemwide functions requiring the sudo scripts.
This commit is contained in:
parent
4d6f810100
commit
1baaa4042c
11 changed files with 287 additions and 104 deletions
|
|
@ -3,6 +3,11 @@
|
||||||
|
|
||||||
# 2.4.3
|
# 2.4.3
|
||||||
### Systemwide Killswitch Introduced for Enable
|
### Systemwide Killswitch Introduced for Enable
|
||||||
|
|
||||||
|
### July 23, 2026
|
||||||
|
Separation of performing subprocess commands, parsing them for error strings, and handling those errors as enum types. Systemwide error handling was introduced via a wrapper to all systemwide functions requiring the sudo scripts.
|
||||||
|
<br/>
|
||||||
|
|
||||||
### July 20, 2026
|
### July 20, 2026
|
||||||
Introduced IP-interface-based killswitch support for systemwide wireguard, on enable only. This version is stable and working for enable only, with a "test" script in the fileslot. And fixed GUI's object save creation. Additionally rolled back Connection data models to previous non-enum version.
|
Introduced IP-interface-based killswitch support for systemwide wireguard, on enable only. This version is stable and working for enable only, with a "test" script in the fileslot. And fixed GUI's object save creation. Additionally rolled back Connection data models to previous non-enum version.
|
||||||
<br/>
|
<br/>
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,11 @@ if [[ "$ACTION" == "set" ]]; then
|
||||||
_set_dns
|
_set_dns
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$ACTION" == "revert" ]]; then
|
# if [[ "$ACTION" == "revert" ]]; then
|
||||||
resolvectl revert "$IFACE" 2>/dev/null || true
|
# resolvectl revert "$IFACE" 2>/dev/null || true
|
||||||
|
|
||||||
|
# fi
|
||||||
|
|
||||||
|
if [[ "$ACTION" == "revert" ]]; then
|
||||||
|
resolvectl revert "$IFACE" || true
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ class SystemStateController:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create(profile_id: int, firewalled: bool, dns_set: bool) -> SystemState:
|
def create(profile_id: int, firewalled: bool, dns_set: bool) -> SystemState:
|
||||||
return SystemState(profile_id, firewalled, dns_set).save()
|
current_state = SystemState(profile_id, firewalled, dns_set)
|
||||||
|
current_state.save()
|
||||||
|
return current_state
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_or_create(system_state):
|
def update_or_create(system_state):
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,25 @@
|
||||||
|
from core.models.Result import Result, ResultError
|
||||||
|
|
||||||
|
class SudoScript(Exception):
|
||||||
|
"""There are issues with the sudo scripts"""
|
||||||
|
def __init__(self, result: Result):
|
||||||
|
self.result = result
|
||||||
|
if result.message and result.message is not None:
|
||||||
|
error_msg = result.message
|
||||||
|
else:
|
||||||
|
error_msg = result.error_type
|
||||||
|
super().__init__(error_msg)
|
||||||
|
|
||||||
|
class MissingPreReqs(Exception):
|
||||||
|
"""User can't take an action because they lack the prereqs"""
|
||||||
|
def __init__(self, result: Result):
|
||||||
|
self.result = result
|
||||||
|
if result.message and result.message is not None:
|
||||||
|
error_msg = result.message
|
||||||
|
else:
|
||||||
|
error_msg = result.error_type
|
||||||
|
super().__init__(error_msg)
|
||||||
|
|
||||||
|
|
||||||
class TorServiceInitializationError(Exception):
|
class TorServiceInitializationError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ class ResultError(Enum):
|
||||||
SUCCESS = "success"
|
SUCCESS = "success"
|
||||||
NOT_SUPPORTED = "NOT_SUPPORTED"
|
NOT_SUPPORTED = "NOT_SUPPORTED"
|
||||||
INVALID_INPUT = "invalid_input"
|
INVALID_INPUT = "invalid_input"
|
||||||
|
MISSING_FILE = "missing_file"
|
||||||
|
MISSING_DEPENDENCY = "missing_dependency"
|
||||||
CONNECTION = "connection"
|
CONNECTION = "connection"
|
||||||
DATABASE = "database"
|
DATABASE = "database"
|
||||||
PERMISSION = "permission"
|
PERMISSION = "permission"
|
||||||
|
|
@ -17,6 +19,8 @@ class ResultError(Enum):
|
||||||
FIREWALL = "firewall"
|
FIREWALL = "firewall"
|
||||||
CLIENT_DNS = "client_dns"
|
CLIENT_DNS = "client_dns"
|
||||||
EXTERNAL_DNS = "external_dns"
|
EXTERNAL_DNS = "external_dns"
|
||||||
|
INTERFACE = "interface"
|
||||||
|
TIMEOUT = "timeout"
|
||||||
UNKNOWN = "unknown"
|
UNKNOWN = "unknown"
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -25,6 +29,7 @@ class Result():
|
||||||
error_type: ResultError = ResultError.SUCCESS
|
error_type: ResultError = ResultError.SUCCESS
|
||||||
data: Optional[Any] = None
|
data: Optional[Any] = None
|
||||||
message: Optional[str] = None
|
message: Optional[str] = None
|
||||||
|
goal: Optional[str] = None
|
||||||
|
|
||||||
def user_message(self) -> str:
|
def user_message(self) -> str:
|
||||||
"""Human-readable error for the UI."""
|
"""Human-readable error for the UI."""
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,49 @@
|
||||||
# custom
|
# custom
|
||||||
|
from core.services.networking.systemwide.systemwide_errors import systemwide_hell_raiser
|
||||||
|
from core.utils.basic_operations.wrap_with import wrap_with
|
||||||
|
from core.utils.basic_operations.run_generic_command import run_generic_command
|
||||||
from core.Constants import Constants
|
from core.Constants import Constants
|
||||||
from core.models.Result import Result, ResultError
|
from core.models.Result import Result, ResultError
|
||||||
from core.errors.logger import logger
|
from core.errors.logger import logger
|
||||||
|
|
||||||
import subprocess
|
|
||||||
from subprocess import CalledProcessError
|
|
||||||
|
|
||||||
DNS_SCRIPT = f"/opt/hydra-veil/dns"
|
DNS_SCRIPT = f"/opt/hydra-veil/dns"
|
||||||
|
|
||||||
|
|
||||||
# requires script to have sudo
|
# requires script to have sudo
|
||||||
|
@wrap_with(systemwide_hell_raiser)
|
||||||
def set_on(network_interface: str, dns_should_be: str) -> Result:
|
def set_on(network_interface: str, dns_should_be: str) -> Result:
|
||||||
result = subprocess.run(
|
command = ["sudo", DNS_SCRIPT, "set", network_interface, dns_should_be]
|
||||||
["sudo", DNS_SCRIPT, "set", network_interface, dns_should_be],
|
human_readable_goal = "Set DNS"
|
||||||
capture_output=True,
|
return run_generic_command(command, human_readable_goal)
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
logger.info("DNS setup successful")
|
|
||||||
return Result(valid=True)
|
|
||||||
else:
|
|
||||||
logger.error(f"DNS setup failed: {result.stderr.strip()}")
|
|
||||||
return Result(valid=False, message=result.stderr.strip())
|
|
||||||
|
|
||||||
# requires script to have sudo
|
# requires script to have sudo
|
||||||
|
@wrap_with(systemwide_hell_raiser)
|
||||||
def revert() -> Result:
|
def revert() -> Result:
|
||||||
try:
|
command = ["sudo", DNS_SCRIPT, "revert"]
|
||||||
result = subprocess.run(
|
human_readable_goal = "DNS revert"
|
||||||
["sudo", DNS_SCRIPT, "revert"],
|
return run_generic_command(command, human_readable_goal)
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
logger.info("DNS revert successful")
|
|
||||||
return Result(valid=True)
|
|
||||||
else:
|
|
||||||
logger.error(f"DNS revert failed: {result.stderr.strip()}")
|
|
||||||
return Result(valid=False, message=result.stderr.strip())
|
|
||||||
except CalledProcessError as e:
|
|
||||||
return Result(valid=False, message=e)
|
|
||||||
|
|
||||||
|
|
||||||
# does NOT need sudo
|
# does NOT need sudo
|
||||||
def is_systemd_enabled() -> Result:
|
def is_systemd_enabled() -> Result:
|
||||||
try:
|
command = ["systemctl", "is-enabled", "systemd-resolved"]
|
||||||
process = subprocess.Popen(('systemctl', 'is-enabled', 'systemd-resolved'), stdout=subprocess.PIPE)
|
human_readable_goal = "Find out if SystemD is enabled"
|
||||||
process_output = str(process.stdout.read())
|
result = run_generic_command(command, human_readable_goal, timeout=5)
|
||||||
if 'enabled' in process_output:
|
if result.valid:
|
||||||
|
if "enabled" in result.data:
|
||||||
return Result(valid=True)
|
return Result(valid=True)
|
||||||
else:
|
else:
|
||||||
return Result(valid=False, message=process_output)
|
logger.error("Systemctl is not even enabled.")
|
||||||
except CalledProcessError as e:
|
return Result(valid=False) # this would be true in the original object just because the command ran.
|
||||||
return Result(valid=False, message=e)
|
else:
|
||||||
|
return result # send the error itself.
|
||||||
|
|
||||||
|
|
||||||
# does NOT need sudo
|
# does NOT need sudo
|
||||||
def get_resolvctl_data(which_command: str) -> Result:
|
def get_resolvctl_data(which_command: str) -> Result:
|
||||||
try:
|
command = ['resolvectl', which_command]
|
||||||
process_output = subprocess.check_output(('resolvectl', which_command), text=True)
|
human_readable_goal = "Get Resolvctl Data"
|
||||||
return Result(valid=True, data=process_output)
|
return run_generic_command(command, human_readable_goal, timeout=5)
|
||||||
|
|
||||||
except CalledProcessError as e:
|
|
||||||
return Result(valid=False, message=e)
|
|
||||||
|
|
||||||
|
|
||||||
# These are the raw Linux systemctl commands:
|
# These are the raw Linux systemctl commands:
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,37 @@
|
||||||
import subprocess
|
from core.services.networking.systemwide.systemwide_errors import systemwide_hell_raiser
|
||||||
|
from core.utils.basic_operations.wrap_with import wrap_with
|
||||||
|
from core.utils.basic_operations.run_generic_command import run_generic_command
|
||||||
from core.models.Result import Result, ResultError
|
from core.models.Result import Result, ResultError
|
||||||
from core.errors.logger import logger
|
from core.errors.logger import logger
|
||||||
|
import subprocess
|
||||||
|
|
||||||
KILLSWITCH_WRAPPER = "/opt/hydra-veil/firewall" # needs chmod 755
|
KILLSWITCH_WRAPPER = "/opt/hydra-veil/firewall" # needs chmod 755
|
||||||
|
|
||||||
|
@wrap_with(systemwide_hell_raiser)
|
||||||
def arm(server_ip: str, tunnel_if: str, internal_subnet: str = None) -> Result:
|
def arm(server_ip: str, tunnel_if: str, internal_subnet: str = None) -> Result:
|
||||||
cmd = ["sudo", KILLSWITCH_WRAPPER, "arm", server_ip, tunnel_if]
|
command = ["sudo", KILLSWITCH_WRAPPER, "arm", server_ip, tunnel_if]
|
||||||
if internal_subnet:
|
if internal_subnet:
|
||||||
cmd.append(internal_subnet)
|
command.append(internal_subnet)
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
human_readable_goal = "Arming the Firewall"
|
||||||
if result.returncode != 0:
|
return run_generic_command(command, human_readable_goal)
|
||||||
error_log = f"Failed to arm firewall: {result.stderr.strip()}"
|
|
||||||
logger.error(f"[KILLSWITCH] {error_log}")
|
|
||||||
return Result(valid=False, error_type=ResultError.FIREWALL, message=error_log)
|
|
||||||
else:
|
|
||||||
return Result(valid=True)
|
|
||||||
|
|
||||||
|
|
||||||
|
@wrap_with(systemwide_hell_raiser)
|
||||||
def disarm() -> bool:
|
def disarm() -> bool:
|
||||||
result = subprocess.run(
|
command = ["sudo", KILLSWITCH_WRAPPER, "disarm"]
|
||||||
["sudo", KILLSWITCH_WRAPPER, "disarm"],
|
human_readable_goal = "Disarming the Firewall"
|
||||||
capture_output=True,
|
return run_generic_command(command, human_readable_goal)
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
|
||||||
error_msg = f"Failed to disarm: {result.stderr.strip()}"
|
|
||||||
logger.error(f"[KILLSWITCH] {error_log}")
|
|
||||||
return Result(valid=False, error_type=ResultError.FIREWALL, message=error_log)
|
|
||||||
else:
|
|
||||||
return Result(valid=True)
|
|
||||||
|
|
||||||
|
|
||||||
def status() -> bool:
|
def status() -> bool:
|
||||||
result = subprocess.run(
|
command = ["sudo", KILLSWITCH_WRAPPER, "status"]
|
||||||
["sudo", KILLSWITCH_WRAPPER, "status"],
|
human_readable_goal = "Get the Firewall's status"
|
||||||
capture_output=True,
|
result = run_generic_command(command, human_readable_goal, timeout=5)
|
||||||
text=True,
|
if result.valid:
|
||||||
)
|
if "armed" in result.data.strip():
|
||||||
return result.returncode == 0 and result.stdout.strip() == "armed"
|
return True
|
||||||
|
else:
|
||||||
|
logger.error("[FIREWALL STATUS] It is currently NOT armed.")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# process error with running the command itself,
|
||||||
|
# send the error object with details.
|
||||||
|
return result
|
||||||
|
|
|
||||||
54
core/services/networking/systemwide/systemwide_errors.py
Normal file
54
core/services/networking/systemwide/systemwide_errors.py
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
from core.errors.exceptions import SudoScript, MissingPreReqs
|
||||||
|
from core.models.Result import Result, ResultError
|
||||||
|
from core.errors.logger import logger
|
||||||
|
|
||||||
|
"""
|
||||||
|
Purpose:
|
||||||
|
Raises Errors for known problems with the systemwide connections
|
||||||
|
|
||||||
|
Kinds of Raises:
|
||||||
|
SudoScript
|
||||||
|
MissingPreReqs
|
||||||
|
"""
|
||||||
|
|
||||||
|
def systemwide_hell_raiser(result: Result) -> Result:
|
||||||
|
if result.valid:
|
||||||
|
return result
|
||||||
|
|
||||||
|
logger.error(f"[HELL RAISER] We were trying to {result.goal} but got an error of {result.error_type} with a human message of {result.message}")
|
||||||
|
|
||||||
|
if result.error_type == ResultError.MISSING_FILE:
|
||||||
|
logger.error(f"[HELL RAISER] We are missing the setup scripts for {result.goal}")
|
||||||
|
result.message = f"You're missing the setup scripts for {result.goal}. Re-do the install, or check the setup you did."
|
||||||
|
raise SudoScript(result)
|
||||||
|
|
||||||
|
elif result.error_type == ResultError.PERMISSION:
|
||||||
|
error_msg = f"There's improper permissions on the scripts for {result.goal}. Re-do the install, or check the setup you did."
|
||||||
|
result.message = error_msg
|
||||||
|
logger.error(f"[HELL RAISER] {error_msg}")
|
||||||
|
raise SudoScript(result)
|
||||||
|
|
||||||
|
elif result.error_type == ResultError.MISSING_DEPENDENCY:
|
||||||
|
error_output = result.message
|
||||||
|
list_of_dependencies = ["nmcli", "resolvconf", "nftables", "nft"]
|
||||||
|
what_is_missing = []
|
||||||
|
|
||||||
|
for each_dependency in list_of_dependencies:
|
||||||
|
if each_dependency in error_output:
|
||||||
|
what_is_missing.append(each_dependency)
|
||||||
|
|
||||||
|
result.message = f"You're missing the prereq packages of {what_is_missing}. Re-do the install, or check the setup you did."
|
||||||
|
raise MissingPreReqs(result)
|
||||||
|
|
||||||
|
elif result.error_type == ResultError.INTERFACE:
|
||||||
|
logger.info("The Interface is not found.")
|
||||||
|
# might not be an error, if it's already down,
|
||||||
|
return result
|
||||||
|
|
||||||
|
elif result.error_type == ResultError.UNKNOWN:
|
||||||
|
logger.info(f"Unknown error type for {result.goal} with {result.message} and potentially output of {result.data}.")
|
||||||
|
return result
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.info("Unknown Result object")
|
||||||
|
return result
|
||||||
|
|
@ -53,7 +53,7 @@ def revert_dns() -> Result:
|
||||||
turn_off = dns.revert()
|
turn_off = dns.revert()
|
||||||
logger.info(f"The result of the turn off is {turn_off.valid}")
|
logger.info(f"The result of the turn off is {turn_off.valid}")
|
||||||
if not turn_off.valid:
|
if not turn_off.valid:
|
||||||
logger.error(f"Critical DNS Error! Could NOT turn off DNS. {turn_off.message}")
|
logger.error(f"Critical DNS Error! Could NOT turn off DNS. Error type: {turn_off.error_type} with {turn_off.message}")
|
||||||
return turn_off.valid
|
return turn_off.valid
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -100,6 +100,8 @@ def check_and_kill_firewall():
|
||||||
else:
|
else:
|
||||||
error_msg = f"Firewall can't be disabled: {disable_result_object.message}"
|
error_msg = f"Firewall can't be disabled: {disable_result_object.message}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
|
|
||||||
|
# if the systemwide_raiser didn't catch it with the reason, then..
|
||||||
raise ConnectionTerminationError(error_msg)
|
raise ConnectionTerminationError(error_msg)
|
||||||
else:
|
else:
|
||||||
logger.info("We are skipping disabling the firewall, because it's already off.")
|
logger.info("We are skipping disabling the firewall, because it's already off.")
|
||||||
|
|
@ -235,6 +237,16 @@ def __establish_system_connection(
|
||||||
if not sinkhole_result:
|
if not sinkhole_result:
|
||||||
raise ConnectionError('IPv6 Could not be protected.')
|
raise ConnectionError('IPv6 Could not be protected.')
|
||||||
|
|
||||||
|
# ============= SETUP STATE =============
|
||||||
|
# Even if firewall is off, we want to save the fact we turned on the VPN tunnel, before we raise errors.
|
||||||
|
|
||||||
|
logger.info(f"Setting State JSON..")
|
||||||
|
current_state = SystemStateController.create(
|
||||||
|
profile_id=profile.id,
|
||||||
|
firewalled=firewall_setting, # intended
|
||||||
|
dns_set=dns_setting # intended
|
||||||
|
)
|
||||||
|
|
||||||
# ============= PREP SETTINGS =============
|
# ============= PREP SETTINGS =============
|
||||||
if firewall_setting or dns_setting:
|
if firewall_setting or dns_setting:
|
||||||
wg_interface_result = extract_wg_interface_name(process_output)
|
wg_interface_result = extract_wg_interface_name(process_output)
|
||||||
|
|
@ -258,6 +270,22 @@ def __establish_system_connection(
|
||||||
if turn_on_result.valid:
|
if turn_on_result.valid:
|
||||||
firewall_tracker = True
|
firewall_tracker = True
|
||||||
|
|
||||||
|
# ============= CONFIRM ON FIREWALL =============
|
||||||
|
# if firewall_setting:
|
||||||
|
logger.info(f"Evaluating Firewall Situation after we tried to turn on it on.")
|
||||||
|
firewall_status = killswitch.status()
|
||||||
|
logger.info(f"""
|
||||||
|
The firewall setting is on.
|
||||||
|
The result of arming it, {turn_on_result.valid}
|
||||||
|
The result of the status {firewall_status}
|
||||||
|
""")
|
||||||
|
if firewall_status:
|
||||||
|
logger.info("Firewall is on. All good.")
|
||||||
|
else:
|
||||||
|
logger.error("Firewall FAILED TO ARM") # DON'T TRY TO RENEGOTIATE WITH DEAD INTERNET !!
|
||||||
|
|
||||||
|
# raise ConnectionError(f'Firewall failed to arm!! {turn_on_result.message}.') # if this triggers, it tries to renegotiate with dead internet.
|
||||||
|
|
||||||
# ============= DNS =============
|
# ============= DNS =============
|
||||||
if dns_setting:
|
if dns_setting:
|
||||||
set_dns_result = set_dns(str(profile.id), wg_interface_name)
|
set_dns_result = set_dns(str(profile.id), wg_interface_name)
|
||||||
|
|
@ -274,39 +302,19 @@ def __establish_system_connection(
|
||||||
did_dns_work = False
|
did_dns_work = False
|
||||||
logger.info("Skipping DNS setup, because it's off in settings")
|
logger.info("Skipping DNS setup, because it's off in settings")
|
||||||
|
|
||||||
# ============= SETUP STATE =============
|
|
||||||
# Even if firewall is off, we want to save the fact we turned on the VPN tunnel, before we raise errors.
|
|
||||||
|
|
||||||
logger.info(f"Setting State JSON..")
|
|
||||||
SystemStateController.create(
|
|
||||||
profile_id=profile.id,
|
|
||||||
firewalled=firewall_tracker,
|
|
||||||
dns_set=did_dns_work
|
|
||||||
)
|
|
||||||
|
|
||||||
# ============= CONFIRM ON FIREWALL =============
|
|
||||||
if firewall_setting:
|
|
||||||
logger.info(f"Evaluating Firewall Situation after we tried to turn on it on.")
|
|
||||||
firewall_status = killswitch.status()
|
|
||||||
logger.info(f"""
|
|
||||||
The firewall setting is on.
|
|
||||||
The result of arming it, {turn_on_result.valid}
|
|
||||||
The result of the status {firewall_status}
|
|
||||||
""")
|
|
||||||
if firewall_status:
|
|
||||||
logger.info("Firewall is on. All good.")
|
|
||||||
else:
|
|
||||||
logger.error("Firewall FAILED TO ARM") # TRIES TO RENEGOTIATE WITH DEAD INTERNET !!
|
|
||||||
# raise ConnectionError(f'Firewall failed to arm!! {turn_on_result.message}.') # if this triggers, it tries to renegotiate with dead internet.
|
|
||||||
|
|
||||||
logger.info("If we made it this far, we have nmcli on, ipv6 sinkhole on, firewall on.")
|
|
||||||
# ============= TESTING =============
|
# ============= TESTING =============
|
||||||
|
logger.info("If we made it this far, we have nmcli on, ipv6 sinkhole on, firewall on.")
|
||||||
try:
|
try:
|
||||||
await_connection(connection_observer=connection_observer)
|
await_connection(connection_observer=connection_observer)
|
||||||
|
|
||||||
except ConnectionError:
|
except ConnectionError:
|
||||||
raise ConnectionError('The connection could not be established.')
|
raise ConnectionError('The connection could not be established.')
|
||||||
|
|
||||||
|
# ============= UPDATE STATE =============
|
||||||
|
current_state.firewalled = firewall_tracker
|
||||||
|
current_state.dns_set = did_dns_work
|
||||||
|
SystemStateController.update_or_create(current_state)
|
||||||
|
|
||||||
return Result(valid=True)
|
return Result(valid=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -339,7 +347,8 @@ def establish_system_connection(
|
||||||
|
|
||||||
# ================= SETTINGS =================
|
# ================= SETTINGS =================
|
||||||
firewall_setting = get_firewall_setting()
|
firewall_setting = get_firewall_setting()
|
||||||
dns_setting = get_dns_setting()
|
dns_setting = True # for testing
|
||||||
|
# dns_setting = get_dns_setting()
|
||||||
|
|
||||||
# ================= CONNECT WG ================
|
# ================= CONNECT WG ================
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
66
core/utils/basic_operations/run_generic_command.py
Normal file
66
core/utils/basic_operations/run_generic_command.py
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
from core.models.Result import Result, ResultError
|
||||||
|
from core.errors.logger import logger
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
|
||||||
|
_SUDO_KW = {
|
||||||
|
"stdout": subprocess.PIPE,
|
||||||
|
"stderr": subprocess.PIPE,
|
||||||
|
"stdin": subprocess.DEVNULL,
|
||||||
|
"env": {**os.environ, "SUDO_ASKPASS": "/bin/false"},
|
||||||
|
"text": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_errors(error_output: str) -> ResultError:
|
||||||
|
# Interface missing
|
||||||
|
if "No such device" in error_output:
|
||||||
|
error_enum = ResultError.INTERFACE
|
||||||
|
# Permission error
|
||||||
|
elif "No permissions" in error_output:
|
||||||
|
error_enum = ResultError.PERMISSION
|
||||||
|
elif "sudo: a password is required" or "password is required" in error_output:
|
||||||
|
error_enum = ResultError.PERMISSION
|
||||||
|
elif "command not found" in error_output:
|
||||||
|
error_enum = ResultError.MISSING_DEPENDENCY
|
||||||
|
else:
|
||||||
|
error_enum = ResultError.UNKNOWN
|
||||||
|
return error_enum
|
||||||
|
|
||||||
|
|
||||||
|
def run_generic_command(
|
||||||
|
command: list,
|
||||||
|
human_readable_goal: str,
|
||||||
|
timeout: float | None = None
|
||||||
|
) -> Result:
|
||||||
|
|
||||||
|
output_data = None # incase command has nothing.
|
||||||
|
|
||||||
|
# Add -n flag to sudo for non-interactive mode (no prompts)
|
||||||
|
if command and command[0] == "sudo":
|
||||||
|
if "-n" not in command:
|
||||||
|
command = [command[0], "-n"] + command[1:]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
command,
|
||||||
|
**_SUDO_KW,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
output_data = result.stdout
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.info(f"{human_readable_goal} was successful")
|
||||||
|
return Result(valid=True, data=output_data)
|
||||||
|
else:
|
||||||
|
error_output = result.stderr.strip()
|
||||||
|
error_enum = parse_errors(error_output)
|
||||||
|
logger.error(f"{human_readable_goal} Failed, error type is {error_enum}.")
|
||||||
|
return Result(valid=False, error_type=error_enum, goal=human_readable_goal, message=error_output)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return Result(valid=False, error_type=ResultError.TIMEOUT, goal=human_readable_goal, data=output_data, message="Command timed out")
|
||||||
|
except FileNotFoundError:
|
||||||
|
return Result(valid=False, error_type=ResultError.MISSING_FILE, goal=human_readable_goal, data=output_data, message="Script is not found")
|
||||||
|
except Exception as e:
|
||||||
|
return Result(valid=False, error_type=ResultError.UNKNOWN, goal=human_readable_goal, data=output_data, message=e)
|
||||||
|
|
||||||
40
core/utils/basic_operations/wrap_with.py
Normal file
40
core/utils/basic_operations/wrap_with.py
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TypeVar, Callable
|
||||||
|
import functools
|
||||||
|
|
||||||
|
"""
|
||||||
|
Purpose:
|
||||||
|
Wraps one function with another
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
1) Takes the original function it's wrapping around, and does it
|
||||||
|
2) Passes that into the second function `wrapper_fn`
|
||||||
|
3) Returns the resulting object type
|
||||||
|
|
||||||
|
Assumes:
|
||||||
|
Same object/type for both functions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
T = TypeVar('T') # Input type (what the original function returns)
|
||||||
|
U = TypeVar('U') # Output type (what wrapper_fn returns)
|
||||||
|
|
||||||
|
def wrap_with(wrapper_fn: Callable[[T], U]) -> Callable:
|
||||||
|
def decorator(func: Callable[..., T]) -> Callable[..., U]:
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs) -> U:
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
return wrapper_fn(result)
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
# def wrap_with(wrapper_fn: Callable[[Result], Result]) -> Callable:
|
||||||
|
# def decorator(func: Callable[..., Result]) -> Callable[..., Result]:
|
||||||
|
# @functools.wraps(func)
|
||||||
|
# def wrapper(*args, **kwargs) -> Result:
|
||||||
|
# result = func(*args, **kwargs)
|
||||||
|
# processed_result = wrapper_fn(result) # ← Any function plugged in
|
||||||
|
# return processed_result
|
||||||
|
# return wrapper
|
||||||
|
# return decorator
|
||||||
|
|
||||||
Loading…
Reference in a new issue