| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- import os
- import json
- import socket
- import urllib.request
- from typing import Dict, Any
- from pathlib import Path
- import subprocess
- import platform
- import tempfile
- from datetime import datetime
- import psutil
- import machineid # https://github.com/keygen-sh/py-machineid
- from rich import print
- PACKAGE_DIR = Path(__file__).parent
- DATA_DIR = Path(os.getcwd()).resolve()
- def get_vm_info():
- hw_in_docker = bool(os.getenv('IN_DOCKER', False) in ('1', 'true', 'True', 'TRUE'))
- hw_in_vm = False
- try:
- # check for traces of docker/containerd/podman in cgroup
- with open('/proc/self/cgroup', 'r') as procfile:
- for line in procfile:
- cgroup = line.strip() # .split('/', 1)[-1].lower()
- if 'docker' in cgroup or 'containerd' in cgroup or 'podman' in cgroup:
- hw_in_docker = True
- except Exception:
- pass
-
- hw_manufacturer = 'Docker' if hw_in_docker else 'Unknown'
- hw_product = 'Container' if hw_in_docker else 'Unknown'
- hw_uuid = machineid.id()
-
- if platform.system().lower() == 'darwin':
- # Get macOS machine info
- hw_manufacturer = 'Apple'
- hw_product = 'Mac'
- try:
- # Hardware:
- # Hardware Overview:
- # Model Name: Mac Studio
- # Model Identifier: Mac13,1
- # Model Number: MJMV3LL/A
- # ...
- # Serial Number (system): M230YYTD77
- # Hardware UUID: 39A12B50-1972-5910-8BEE-235AD20C8EE3
- # ...
- result = subprocess.run(['system_profiler', 'SPHardwareDataType'], capture_output=True, text=True, check=True)
- for line in result.stdout.split('\n'):
- if 'Model Name:' in line:
- hw_product = line.split(':', 1)[-1].strip()
- elif 'Model Identifier:' in line:
- hw_product += ' ' + line.split(':', 1)[-1].strip()
- elif 'Hardware UUID:' in line:
- hw_uuid = line.split(':', 1)[-1].strip()
- except Exception:
- pass
- else:
- # get Linux machine info
- try:
- # Getting SMBIOS data from sysfs.
- # SMBIOS 2.8 present.
- # argo-1 | 2024-10-01T10:40:51Z ERR error="Incoming request ended abruptly: context canceled" connIndex=2 event=1 ingressRule=0 originService=http://archivebox:8000 │
- # Handle 0x0100, DMI type 1, 27 bytes
- # System Information
- # Manufacturer: DigitalOcean
- # Product Name: Droplet
- # Serial Number: 411922099
- # UUID: fb65f41c-ec24-4539-beaf-f941903bdb2c
- # ...
- # Family: DigitalOcean_Droplet
- dmidecode = subprocess.run(['dmidecode', '-t', 'system'], capture_output=True, text=True, check=True)
- for line in dmidecode.stdout.split('\n'):
- if 'Manufacturer:' in line:
- hw_manufacturer = line.split(':', 1)[-1].strip()
- elif 'Product Name:' in line:
- hw_product = line.split(':', 1)[-1].strip()
- elif 'UUID:' in line:
- hw_uuid = line.split(':', 1)[-1].strip()
- except Exception:
- pass
- # Check for VM fingerprint in manufacturer/product name
- if 'qemu' in hw_product.lower() or 'vbox' in hw_product.lower() or 'lxc' in hw_product.lower() or 'vm' in hw_product.lower():
- hw_in_vm = True
-
- # Check for QEMU explicitly in pmap output
- try:
- result = subprocess.run(['pmap', '1'], capture_output=True, text=True, check=True)
- if 'qemu' in result.stdout.lower():
- hw_in_vm = True
- except Exception:
- pass
- return {
- "hw_in_docker": hw_in_docker,
- "hw_in_vm": hw_in_vm,
- "hw_manufacturer": hw_manufacturer,
- "hw_product": hw_product,
- "hw_uuid": hw_uuid,
- }
- def get_public_ip() -> str:
- def fetch_url(url: str) -> str:
- with urllib.request.urlopen(url, timeout=5) as response:
- return response.read().decode('utf-8').strip()
- def fetch_dns(pubip_lookup_host: str) -> str:
- return socket.gethostbyname(pubip_lookup_host).strip()
- methods = [
- (lambda: fetch_url("https://ipinfo.io/ip"), lambda r: r),
- (lambda: fetch_url("https://api.ipify.org?format=json"), lambda r: json.loads(r)['ip']),
- (lambda: fetch_dns("myip.opendns.com"), lambda r: r),
- (lambda: fetch_url("http://whatismyip.akamai.com/"), lambda r: r), # try HTTP as final fallback in case of TLS/system time errors
- ]
- for fetch, parse in methods:
- try:
- result = parse(fetch())
- if result:
- return result
- except Exception:
- continue
- raise Exception("Could not determine public IP address")
- def get_local_ip(remote_ip: str='1.1.1.1', remote_port: int=80) -> str:
- try:
- with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
- s.connect((remote_ip, remote_port))
- return s.getsockname()[0]
- except Exception:
- pass
- return '127.0.0.1'
- ip_addrs = lambda addrs: (a for a in addrs if a.family == socket.AF_INET)
- mac_addrs = lambda addrs: (a for a in addrs if a.family == psutil.AF_LINK)
- def get_isp_info(ip=None):
- # Get public IP
- try:
- ip = ip or urllib.request.urlopen('https://api.ipify.org').read().decode('utf8')
- except Exception:
- pass
-
- # Get ISP name, city, and country
- data = {}
- try:
- url = f'https://ipapi.co/{ip}/json/'
- response = urllib.request.urlopen(url)
- data = json.loads(response.read().decode())
- except Exception:
- pass
-
- isp = data.get('org', 'Unknown')
- city = data.get('city', 'Unknown')
- region = data.get('region', 'Unknown')
- country = data.get('country_name', 'Unknown')
-
- # Get system DNS resolver servers
- dns_server = None
- try:
- result = subprocess.run(['dig', 'example.com', 'A'], capture_output=True, text=True, check=True).stdout
- dns_server = result.split(';; SERVER: ', 1)[-1].split('\n')[0].split('#')[0].strip()
- except Exception:
- try:
- dns_server = Path('/etc/resolv.conf').read_text().split('nameserver ', 1)[-1].split('\n')[0].strip()
- except Exception:
- dns_server = '127.0.0.1'
- print(f'[red]:warning: WARNING: Could not determine DNS server, using {dns_server}[/red]')
-
- # Get DNS resolver's ISP name
- # url = f'https://ipapi.co/{dns_server}/json/'
- # dns_isp = json.loads(urllib.request.urlopen(url).read().decode()).get('org', 'Unknown')
-
- return {
- 'isp': isp,
- 'city': city,
- 'region': region,
- 'country': country,
- 'dns_server': dns_server,
- # 'net_dns_isp': dns_isp,
- }
-
- def get_host_network() -> Dict[str, Any]:
- default_gateway_local_ip = get_local_ip()
- gateways = psutil.net_if_addrs()
-
- for interface, ips in gateways.items():
- for local_ip in ip_addrs(ips):
- if default_gateway_local_ip == local_ip.address:
- mac_address = next(mac_addrs(ips)).address
- public_ip = get_public_ip()
- return {
- "hostname": max([socket.gethostname(), platform.node()], key=len),
- "iface": interface,
- "mac_address": mac_address,
- "ip_local": local_ip.address,
- "ip_public": public_ip,
- # "is_behind_nat": local_ip.address != public_ip,
- **get_isp_info(public_ip),
- }
-
- raise Exception("Could not determine host network info")
- def get_os_info() -> Dict[str, Any]:
- os_release = platform.release()
- if platform.system().lower() == 'darwin':
- os_release = 'macOS ' + platform.mac_ver()[0]
- else:
- try:
- os_release = subprocess.run(['lsb_release', '-ds'], capture_output=True, text=True, check=True).stdout.strip()
- except Exception:
- pass
-
- return {
- "os_arch": platform.machine(),
- "os_family": platform.system().lower(),
- "os_platform": platform.platform(),
- "os_kernel": platform.version(),
- "os_release": os_release,
- }
- def get_host_stats() -> Dict[str, Any]:
- with tempfile.TemporaryDirectory() as tmp_dir:
- tmp_usage = psutil.disk_usage(str(tmp_dir))
- app_usage = psutil.disk_usage(str(PACKAGE_DIR))
- data_usage = psutil.disk_usage(str(DATA_DIR))
- mem_usage = psutil.virtual_memory()
- swap_usage = psutil.swap_memory()
- return {
- "cpu_boot_time": datetime.fromtimestamp(psutil.boot_time()).isoformat(),
- "cpu_count": psutil.cpu_count(logical=False),
- "cpu_load": psutil.getloadavg(),
- # "cpu_pct": psutil.cpu_percent(interval=1),
- "mem_virt_used_pct": mem_usage.percent,
- "mem_virt_used_gb": round(mem_usage.used / 1024 / 1024 / 1024, 3),
- "mem_virt_free_gb": round(mem_usage.free / 1024 / 1024 / 1024, 3),
- "mem_swap_used_pct": swap_usage.percent,
- "mem_swap_used_gb": round(swap_usage.used / 1024 / 1024 / 1024, 3),
- "mem_swap_free_gb": round(swap_usage.free / 1024 / 1024 / 1024, 3),
- "disk_tmp_used_pct": tmp_usage.percent,
- "disk_tmp_used_gb": round(tmp_usage.used / 1024 / 1024 / 1024, 3),
- "disk_tmp_free_gb": round(tmp_usage.free / 1024 / 1024 / 1024, 3), # in GB
- "disk_app_used_pct": app_usage.percent,
- "disk_app_used_gb": round(app_usage.used / 1024 / 1024 / 1024, 3),
- "disk_app_free_gb": round(app_usage.free / 1024 / 1024 / 1024, 3),
- "disk_data_used_pct": data_usage.percent,
- "disk_data_used_gb": round(data_usage.used / 1024 / 1024 / 1024, 3),
- "disk_data_free_gb": round(data_usage.free / 1024 / 1024 / 1024, 3),
- }
- def get_host_immutable_info(host_info: Dict[str, Any]) -> Dict[str, Any]:
- return {
- key: value
- for key, value in host_info.items()
- if key in ['guid', 'net_mac', 'os_family', 'cpu_arch']
- }
-
- def get_host_guid() -> str:
- return machineid.hashed_id('archivebox')
- # Example usage
- if __name__ == "__main__":
- host_info = {
- 'guid': get_host_guid(),
- 'os': get_os_info(),
- 'vm': get_vm_info(),
- 'net': get_host_network(),
- 'stats': get_host_stats(),
- }
- print(host_info)
- # {
- # 'guid': '1cd2dd279f8a854...6943f2384437991a',
- # 'os': {
- # 'os_arch': 'arm64',
- # 'os_family': 'darwin',
- # 'os_platform': 'macOS-14.6.1-arm64-arm-64bit',
- # 'os_kernel': 'Darwin Kernel Version 23.6.0: Mon Jul 29 21:14:30 PDT 2024; root:xnu-10063.141.2~1/RELEASE_ARM64_T6000',
- # 'os_release': 'macOS 14.6.1'
- # },
- # 'vm': {'hw_in_docker': False, 'hw_in_vm': False, 'hw_manufacturer': 'Apple', 'hw_product': 'Mac Studio Mac13,1', 'hw_uuid': '39A12B50-...-...-...-...'},
- # 'net': {
- # 'hostname': 'somehost.sub.example.com',
- # 'iface': 'en0',
- # 'mac_address': 'ab:cd:ef:12:34:56',
- # 'ip_local': '192.168.2.18',
- # 'ip_public': '123.123.123.123',
- # 'isp': 'AS-SONICTELECOM',
- # 'city': 'Berkeley',
- # 'region': 'California',
- # 'country': 'United States',
- # 'dns_server': '192.168.1.1'
- # },
- # 'stats': {
- # 'cpu_boot_time': '2024-09-24T21:20:16',
- # 'cpu_count': 10,
- # 'cpu_load': (2.35693359375, 4.013671875, 4.1171875),
- # 'mem_virt_used_pct': 66.0,
- # 'mem_virt_used_gb': 15.109,
- # 'mem_virt_free_gb': 0.065,
- # 'mem_swap_used_pct': 89.4,
- # 'mem_swap_used_gb': 8.045,
- # 'mem_swap_free_gb': 0.955,
- # 'disk_tmp_used_pct': 26.0,
- # 'disk_tmp_used_gb': 113.1,
- # 'disk_tmp_free_gb': 322.028,
- # 'disk_app_used_pct': 56.1,
- # 'disk_app_used_gb': 2138.796,
- # 'disk_app_free_gb': 1675.996,
- # 'disk_data_used_pct': 56.1,
- # 'disk_data_used_gb': 2138.796,
- # 'disk_data_free_gb': 1675.996
- # }
- # }
|