199 lines
5.7 KiB
Python
199 lines
5.7 KiB
Python
import os
|
|
import random
|
|
import select
|
|
import signal
|
|
import socket
|
|
import ssl
|
|
import struct
|
|
import time
|
|
from types import FrameType, TracebackType
|
|
from typing import Self
|
|
from urllib.error import URLError
|
|
from urllib.parse import urlencode
|
|
from urllib.request import urlopen
|
|
|
|
from archinstall.lib.exceptions import DownloadTimeout, SysCallError
|
|
from archinstall.lib.log import debug, error, info
|
|
from archinstall.lib.pacman.pacman import Pacman
|
|
|
|
|
|
class DownloadTimer:
|
|
"""
|
|
Context manager for timing downloads with timeouts.
|
|
"""
|
|
|
|
def __init__(self, timeout: int = 5):
|
|
"""
|
|
Args:
|
|
timeout:
|
|
The download timeout in seconds. The DownloadTimeout exception
|
|
will be raised in the context after this many seconds.
|
|
"""
|
|
self.time: float | None = None
|
|
self.start_time: float | None = None
|
|
self.timeout = timeout
|
|
self.previous_handler = None
|
|
self.previous_timer: int | None = None
|
|
|
|
def raise_timeout(self, signl: int, frame: FrameType | None) -> None:
|
|
"""
|
|
Raise the DownloadTimeout exception.
|
|
"""
|
|
raise DownloadTimeout(f'Download timed out after {self.timeout} second(s).')
|
|
|
|
def __enter__(self) -> Self:
|
|
if self.timeout > 0:
|
|
self.previous_handler = signal.signal(signal.SIGALRM, self.raise_timeout) # type: ignore[assignment]
|
|
self.previous_timer = signal.alarm(self.timeout)
|
|
|
|
self.start_time = time.monotonic()
|
|
return self
|
|
|
|
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
|
|
if self.start_time:
|
|
time_delta = time.monotonic() - self.start_time
|
|
signal.alarm(0)
|
|
self.time = time_delta
|
|
if self.timeout > 0:
|
|
signal.signal(signal.SIGALRM, self.previous_handler)
|
|
|
|
previous_timer = self.previous_timer
|
|
if previous_timer and previous_timer > 0:
|
|
remaining_time = int(previous_timer - time_delta)
|
|
# The alarm should have been raised during the download.
|
|
if remaining_time <= 0:
|
|
signal.raise_signal(signal.SIGALRM)
|
|
else:
|
|
signal.alarm(remaining_time)
|
|
self.start_time = None
|
|
|
|
|
|
def get_hw_addr(ifname: str) -> str:
|
|
import fcntl
|
|
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
ret = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', bytes(ifname, 'utf-8')[:15]))
|
|
return ':'.join(f'{b:02x}' for b in ret[18:24])
|
|
|
|
|
|
def list_interfaces(skip_loopback: bool = True) -> dict[str, str]:
|
|
interfaces = {}
|
|
|
|
for _index, iface in socket.if_nameindex():
|
|
if skip_loopback and iface == 'lo':
|
|
continue
|
|
|
|
mac = get_hw_addr(iface).replace(':', '-').lower()
|
|
interfaces[mac] = iface
|
|
|
|
return interfaces
|
|
|
|
|
|
def update_keyring() -> bool:
|
|
info('Updating archlinux-keyring ...')
|
|
try:
|
|
Pacman.run('-Sy --noconfirm archlinux-keyring')
|
|
return True
|
|
except SysCallError:
|
|
if os.geteuid() != 0:
|
|
error("update_keyring() uses 'pacman -Sy archlinux-keyring' which requires root.")
|
|
|
|
return False
|
|
|
|
|
|
def enrich_iface_types(interfaces: list[str]) -> dict[str, str]:
|
|
result = {}
|
|
|
|
for iface in interfaces:
|
|
if os.path.isdir(f'/sys/class/net/{iface}/bridge/'):
|
|
result[iface] = 'BRIDGE'
|
|
elif os.path.isfile(f'/sys/class/net/{iface}/tun_flags'):
|
|
# ethtool -i {iface}
|
|
result[iface] = 'TUN/TAP'
|
|
elif os.path.isdir(f'/sys/class/net/{iface}/device'):
|
|
if os.path.isdir(f'/sys/class/net/{iface}/wireless/'):
|
|
result[iface] = 'WIRELESS'
|
|
else:
|
|
result[iface] = 'PHYSICAL'
|
|
else:
|
|
result[iface] = 'UNKNOWN'
|
|
|
|
return result
|
|
|
|
|
|
def fetch_data_from_url(url: str, params: dict[str, str] | None = None, timeout: int = 30) -> str:
|
|
ssl_context = ssl.create_default_context()
|
|
ssl_context.check_hostname = False
|
|
ssl_context.verify_mode = ssl.CERT_NONE
|
|
|
|
if params is not None:
|
|
encoded = urlencode(params)
|
|
full_url = f'{url}?{encoded}'
|
|
else:
|
|
full_url = url
|
|
|
|
try:
|
|
response = urlopen(full_url, context=ssl_context, timeout=timeout)
|
|
data = response.read().decode('UTF-8')
|
|
return data
|
|
except URLError as e:
|
|
raise ValueError(f'Unable to fetch data from url: {url}\n{e}')
|
|
except Exception as e:
|
|
raise ValueError(f'Unexpected error when parsing response: {e}')
|
|
|
|
|
|
def calc_checksum(icmp_packet: bytes) -> int:
|
|
# Calculate the ICMP checksum
|
|
checksum = 0
|
|
for i in range(0, len(icmp_packet), 2):
|
|
checksum += (icmp_packet[i] << 8) + (struct.unpack('B', icmp_packet[i + 1 : i + 2])[0] if len(icmp_packet[i + 1 : i + 2]) else 0)
|
|
|
|
checksum = (checksum >> 16) + (checksum & 0xFFFF)
|
|
checksum = ~checksum & 0xFFFF
|
|
|
|
return checksum
|
|
|
|
|
|
def build_icmp(payload: bytes) -> bytes:
|
|
# Define the ICMP Echo Request packet
|
|
icmp_packet = struct.pack('!BBHHH', 8, 0, 0, 0, 1) + payload
|
|
|
|
checksum = calc_checksum(icmp_packet)
|
|
|
|
return struct.pack('!BBHHH', 8, 0, checksum, 0, 1) + payload
|
|
|
|
|
|
def ping(hostname: str, timeout: int = 5) -> int:
|
|
watchdog = select.epoll()
|
|
started = time.monotonic()
|
|
random_identifier = f'archinstall-{random.randint(1000, 9999)}'.encode()
|
|
|
|
# Create a raw socket (requires root, which should be fine on archiso)
|
|
icmp_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
|
|
watchdog.register(icmp_socket, select.EPOLLIN | select.EPOLLHUP)
|
|
|
|
icmp_packet = build_icmp(random_identifier)
|
|
|
|
# Send the ICMP packet
|
|
icmp_socket.sendto(icmp_packet, (hostname, 0))
|
|
latency = -1
|
|
|
|
# Gracefully wait for X amount of time
|
|
# for a ICMP response or exit with no latency
|
|
while latency == -1 and time.monotonic() - started < timeout:
|
|
try:
|
|
for _fileno, _event in watchdog.poll(0.1):
|
|
response, _ = icmp_socket.recvfrom(1024)
|
|
icmp_type = struct.unpack('!B', response[20:21])[0]
|
|
|
|
# Check if it's an Echo Reply (ICMP type 0)
|
|
if icmp_type == 0 and response[-len(random_identifier) :] == random_identifier:
|
|
latency = round((time.monotonic() - started) * 1000)
|
|
break
|
|
except OSError as e:
|
|
debug(f'Error: {e}')
|
|
break
|
|
|
|
icmp_socket.close()
|
|
return latency
|