mirror of
https://github.com/ArchiveBox/ArchiveBox.git
synced 2026-01-04 09:55:33 +10:00
add py-machineid lib for new machine app
This commit is contained in:
@@ -29,6 +29,7 @@ from core.mixins import SearchResultsAdminMixin
|
||||
from api.models import APIToken
|
||||
from abid_utils.admin import ABIDModelAdmin
|
||||
from queues.tasks import bg_archive_links, bg_add
|
||||
from machine.models import Machine, NetworkInterface
|
||||
|
||||
from index.html import snapshot_icons
|
||||
from logging_util import printable_filesize
|
||||
@@ -778,3 +779,53 @@ class CustomWebhookAdmin(WebhookAdmin, ABIDModelAdmin):
|
||||
list_display = ('created_at', 'created_by', 'abid', *WebhookAdmin.list_display)
|
||||
sort_fields = ('created_at', 'created_by', 'abid', 'referenced_model', 'endpoint', 'last_success', 'last_error')
|
||||
readonly_fields = ('created_at', 'modified_at', 'abid_info', *WebhookAdmin.readonly_fields)
|
||||
|
||||
|
||||
@admin.register(Machine, site=archivebox_admin)
|
||||
class MachineAdmin(ABIDModelAdmin):
|
||||
list_display = ('abid', 'created_at', 'hostname', 'ips', 'os_platform', 'hw_in_docker', 'hw_in_vm', 'hw_manufacturer', 'hw_product', 'os_arch', 'os_family', 'os_release', 'hw_uuid')
|
||||
sort_fields = ('abid', 'created_at', 'hostname', 'ips', 'os_platform', 'hw_in_docker', 'hw_in_vm', 'hw_manufacturer', 'hw_product', 'os_arch', 'os_family', 'os_release', 'hw_uuid')
|
||||
# search_fields = ('id', 'abid', 'guid', 'hostname', 'hw_manufacturer', 'hw_product', 'hw_uuid', 'os_arch', 'os_family', 'os_platform', 'os_kernel', 'os_release')
|
||||
|
||||
readonly_fields = ('guid', 'created_at', 'modified_at', 'abid_info', 'ips')
|
||||
fields = (*readonly_fields, 'hostname', 'hw_in_docker', 'hw_in_vm', 'hw_manufacturer', 'hw_product', 'hw_uuid', 'os_arch', 'os_family', 'os_platform', 'os_kernel', 'os_release', 'stats')
|
||||
|
||||
list_filter = ('hw_in_docker', 'hw_in_vm', 'os_arch', 'os_family', 'os_platform')
|
||||
ordering = ['-created_at']
|
||||
list_per_page = 100
|
||||
|
||||
@admin.display(
|
||||
description='Public IP',
|
||||
ordering='networkinterface__ip_public',
|
||||
)
|
||||
def ips(self, machine):
|
||||
return format_html(
|
||||
'<a href="/admin/machine/networkinterface/?q={}"><b><code>{}</code></b></a>',
|
||||
machine.abid,
|
||||
', '.join(machine.networkinterface_set.values_list('ip_public', flat=True)),
|
||||
)
|
||||
|
||||
@admin.register(NetworkInterface, site=archivebox_admin)
|
||||
class NetworkInterfaceAdmin(ABIDModelAdmin):
|
||||
list_display = ('abid', 'created_at', 'machine_info', 'ip_public', 'dns_server', 'isp', 'country', 'region', 'city', 'iface', 'ip_local', 'mac_address')
|
||||
sort_fields = ('abid', 'created_at', 'machine_info', 'ip_public', 'dns_server', 'isp', 'country', 'region', 'city', 'iface', 'ip_local', 'mac_address')
|
||||
search_fields = ('abid', 'machine__abid', 'iface', 'ip_public', 'ip_local', 'mac_address', 'dns_server', 'hostname', 'isp', 'city', 'region', 'country')
|
||||
|
||||
readonly_fields = ('machine', 'created_at', 'modified_at', 'abid_info', 'mac_address', 'ip_public', 'ip_local', 'dns_server')
|
||||
fields = (*readonly_fields, 'iface', 'hostname', 'isp', 'city', 'region', 'country')
|
||||
|
||||
list_filter = ('isp', 'country', 'region')
|
||||
ordering = ['-created_at']
|
||||
list_per_page = 100
|
||||
|
||||
@admin.display(
|
||||
description='Machine',
|
||||
ordering='machine__abid',
|
||||
)
|
||||
def machine_info(self, iface):
|
||||
return format_html(
|
||||
'<a href="/admin/machine/machine/{}/change"><b><code>[{}]</code></b> {}</a>',
|
||||
iface.machine.id,
|
||||
iface.machine.abid,
|
||||
iface.machine.hostname,
|
||||
)
|
||||
|
||||
@@ -98,7 +98,8 @@ INSTALLED_APPS = [
|
||||
'django_object_actions', # provides easy Django Admin action buttons on change views https://github.com/crccheck/django-object-actions
|
||||
|
||||
# Our ArchiveBox-provided apps
|
||||
#'config', # ArchiveBox config settings
|
||||
#'config', # ArchiveBox config settings (loaded as a plugin, don't need to add it here)
|
||||
'machine', # handles collecting and storing information about the host machine, network interfaces, installed binaries, etc.
|
||||
'queues', # handles starting and managing background workers and processes
|
||||
'abid_utils', # handles ABID ID creation, handling, and models
|
||||
'core', # core django model with Snapshot, ArchiveResult, etc.
|
||||
|
||||
8
archivebox/machine/apps.py
Normal file
8
archivebox/machine/apps.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MachineConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
|
||||
name = 'machine'
|
||||
verbose_name = 'Machine Info'
|
||||
317
archivebox/machine/detect.py
Normal file
317
archivebox/machine/detect.py
Normal file
@@ -0,0 +1,317 @@
|
||||
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('.').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:
|
||||
pass
|
||||
|
||||
# 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
|
||||
# }
|
||||
# }
|
||||
144
archivebox/machine/migrations/0001_initial.py
Normal file
144
archivebox/machine/migrations/0001_initial.py
Normal file
@@ -0,0 +1,144 @@
|
||||
# Generated by Django 5.1.1 on 2024-10-02 04:34
|
||||
|
||||
import archivebox.abid_utils.models
|
||||
import charidfield.fields
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Machine",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=None,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
unique=True,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"abid",
|
||||
charidfield.fields.CharIDField(
|
||||
blank=True,
|
||||
db_index=True,
|
||||
default=None,
|
||||
help_text="ABID-format identifier for this entity (e.g. snp_01BJQMF54D093DXEAWZ6JYRPAQ)",
|
||||
max_length=30,
|
||||
null=True,
|
||||
prefix="mxn_",
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"created_at",
|
||||
archivebox.abid_utils.models.AutoDateTimeField(
|
||||
db_index=True, default=None
|
||||
),
|
||||
),
|
||||
("modified_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"guid",
|
||||
models.CharField(
|
||||
default=None, editable=False, max_length=64, unique=True
|
||||
),
|
||||
),
|
||||
("hostname", models.CharField(default=None, max_length=63)),
|
||||
("hw_in_docker", models.BooleanField(default=False)),
|
||||
("hw_in_vm", models.BooleanField(default=False)),
|
||||
("hw_manufacturer", models.CharField(default=None, max_length=63)),
|
||||
("hw_product", models.CharField(default=None, max_length=63)),
|
||||
("hw_uuid", models.CharField(default=None, max_length=255)),
|
||||
("os_arch", models.CharField(default=None, max_length=15)),
|
||||
("os_family", models.CharField(default=None, max_length=15)),
|
||||
("os_platform", models.CharField(default=None, max_length=63)),
|
||||
("os_release", models.CharField(default=None, max_length=63)),
|
||||
("os_kernel", models.CharField(default=None, max_length=255)),
|
||||
("stats", models.JSONField(default=None)),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="NetworkInterface",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=None,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
unique=True,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"abid",
|
||||
charidfield.fields.CharIDField(
|
||||
blank=True,
|
||||
db_index=True,
|
||||
default=None,
|
||||
help_text="ABID-format identifier for this entity (e.g. snp_01BJQMF54D093DXEAWZ6JYRPAQ)",
|
||||
max_length=30,
|
||||
null=True,
|
||||
prefix="ixf_",
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"created_at",
|
||||
archivebox.abid_utils.models.AutoDateTimeField(
|
||||
db_index=True, default=None
|
||||
),
|
||||
),
|
||||
("modified_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"mac_address",
|
||||
models.CharField(default=None, editable=False, max_length=17),
|
||||
),
|
||||
(
|
||||
"ip_public",
|
||||
models.GenericIPAddressField(default=None, editable=False),
|
||||
),
|
||||
(
|
||||
"ip_local",
|
||||
models.GenericIPAddressField(default=None, editable=False),
|
||||
),
|
||||
(
|
||||
"dns_server",
|
||||
models.GenericIPAddressField(default=None, editable=False),
|
||||
),
|
||||
("iface", models.CharField(default=None, max_length=15)),
|
||||
("hostname", models.CharField(default=None, max_length=63)),
|
||||
("isp", models.CharField(default=None, max_length=63)),
|
||||
("city", models.CharField(default=None, max_length=63)),
|
||||
("region", models.CharField(default=None, max_length=63)),
|
||||
("country", models.CharField(default=None, max_length=63)),
|
||||
(
|
||||
"machine",
|
||||
models.ForeignKey(
|
||||
default=None,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="machine.machine",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": {
|
||||
("machine", "ip_public", "ip_local", "mac_address", "dns_server")
|
||||
},
|
||||
},
|
||||
),
|
||||
]
|
||||
0
archivebox/machine/migrations/__init__.py
Normal file
0
archivebox/machine/migrations/__init__.py
Normal file
167
archivebox/machine/models.py
Normal file
167
archivebox/machine/models.py
Normal file
@@ -0,0 +1,167 @@
|
||||
__package__ = 'archivebox.machine'
|
||||
|
||||
import socket
|
||||
|
||||
from django.db import models
|
||||
from archivebox.abid_utils.models import ABIDModel, ABIDField, AutoDateTimeField
|
||||
|
||||
from .detect import get_host_guid, get_os_info, get_vm_info, get_host_network, get_host_stats
|
||||
|
||||
CURRENT_MACHINE = None
|
||||
CURRENT_INTERFACE = None
|
||||
|
||||
class MachineManager(models.Manager):
|
||||
def current(self) -> 'Machine':
|
||||
global CURRENT_MACHINE
|
||||
if CURRENT_MACHINE:
|
||||
return CURRENT_MACHINE
|
||||
|
||||
guid = get_host_guid()
|
||||
try:
|
||||
CURRENT_MACHINE = self.get(guid=guid)
|
||||
return CURRENT_MACHINE
|
||||
except self.model.DoesNotExist:
|
||||
pass
|
||||
|
||||
CURRENT_MACHINE = self.model(
|
||||
guid=guid,
|
||||
hostname=socket.gethostname(),
|
||||
**get_os_info(),
|
||||
**get_vm_info(),
|
||||
stats=get_host_stats(),
|
||||
)
|
||||
CURRENT_MACHINE.save()
|
||||
return CURRENT_MACHINE
|
||||
|
||||
class Machine(ABIDModel):
|
||||
abid_prefix = 'mxn_'
|
||||
abid_ts_src = 'self.created_at'
|
||||
abid_uri_src = 'self.guid'
|
||||
abid_subtype_src = '"01"'
|
||||
abid_rand_src = 'self.id'
|
||||
abid_drift_allowed = False
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=None, null=False, editable=False, unique=True, verbose_name='ID')
|
||||
abid = ABIDField(prefix=abid_prefix)
|
||||
|
||||
created_at = AutoDateTimeField(default=None, null=False, db_index=True)
|
||||
modified_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
# IMMUTABLE PROPERTIES
|
||||
guid = models.CharField(max_length=64, default=None, null=False, unique=True, editable=False)
|
||||
|
||||
# MUTABLE PROPERTIES
|
||||
hostname = models.CharField(max_length=63, default=None, null=False)
|
||||
|
||||
hw_in_docker = models.BooleanField(default=False, null=False)
|
||||
hw_in_vm = models.BooleanField(default=False, null=False)
|
||||
hw_manufacturer = models.CharField(max_length=63, default=None, null=False) # e.g. Apple
|
||||
hw_product = models.CharField(max_length=63, default=None, null=False) # e.g. Mac Studio Mac13,1
|
||||
hw_uuid = models.CharField(max_length=255, default=None, null=False) # e.g. 39A12B50-...-...-...-...
|
||||
|
||||
os_arch = models.CharField(max_length=15, default=None, null=False) # e.g. arm64
|
||||
os_family = models.CharField(max_length=15, default=None, null=False) # e.g. darwin
|
||||
os_platform = models.CharField(max_length=63, default=None, null=False) # e.g. macOS-14.6.1-arm64-arm-64bit
|
||||
os_release = models.CharField(max_length=63, default=None, null=False) # e.g. macOS 14.6.1
|
||||
os_kernel = models.CharField(max_length=255, default=None, null=False) # e.g. Darwin Kernel Version 23.6.0: Mon Jul 29 21:14:30 PDT 2024; root:xnu-10063.141.2~1/RELEASE_ARM64_T6000
|
||||
|
||||
stats = models.JSONField(default=None, null=False)
|
||||
|
||||
objects = MachineManager()
|
||||
|
||||
networkinterface_set: models.Manager['NetworkInterface']
|
||||
|
||||
|
||||
class NetworkInterfaceManager(models.Manager):
|
||||
def current(self) -> 'NetworkInterface':
|
||||
global CURRENT_INTERFACE
|
||||
if CURRENT_INTERFACE:
|
||||
return CURRENT_INTERFACE
|
||||
|
||||
machine = Machine.objects.current()
|
||||
net_info = get_host_network()
|
||||
try:
|
||||
CURRENT_INTERFACE = self.get(
|
||||
machine=machine,
|
||||
ip_public=net_info['ip_public'],
|
||||
ip_local=net_info['ip_local'],
|
||||
mac_address=net_info['mac_address'],
|
||||
dns_server=net_info['dns_server'],
|
||||
)
|
||||
return CURRENT_INTERFACE
|
||||
except self.model.DoesNotExist:
|
||||
pass
|
||||
|
||||
CURRENT_INTERFACE = self.model(
|
||||
machine=machine,
|
||||
**get_host_network(),
|
||||
)
|
||||
CURRENT_INTERFACE.save()
|
||||
return CURRENT_INTERFACE
|
||||
|
||||
|
||||
|
||||
class NetworkInterface(ABIDModel):
|
||||
abid_prefix = 'ixf_'
|
||||
abid_ts_src = 'self.machine.created_at'
|
||||
abid_uri_src = 'self.machine.guid'
|
||||
abid_subtype_src = 'self.iface'
|
||||
abid_rand_src = 'self.id'
|
||||
abid_drift_allowed = False
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=None, null=False, editable=False, unique=True, verbose_name='ID')
|
||||
abid = ABIDField(prefix=abid_prefix)
|
||||
|
||||
created_at = AutoDateTimeField(default=None, null=False, db_index=True)
|
||||
modified_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
machine = models.ForeignKey(Machine, on_delete=models.CASCADE, default=None, null=False)
|
||||
|
||||
# IMMUTABLE PROPERTIES
|
||||
mac_address = models.CharField(max_length=17, default=None, null=False, editable=False) # e.g. ab:cd:ef:12:34:56
|
||||
ip_public = models.GenericIPAddressField(default=None, null=False, editable=False) # e.g. 123.123.123.123 or 2001:0db8:85a3:0000:0000:8a2e:0370:7334
|
||||
ip_local = models.GenericIPAddressField(default=None, null=False, editable=False) # e.g. 192.168.2.18 or 2001:0db8:85a3:0000:0000:8a2e:0370:7334
|
||||
dns_server = models.GenericIPAddressField(default=None, null=False, editable=False) # e.g. 8.8.8.8 or 2001:0db8:85a3:0000:0000:8a2e:0370:7334
|
||||
|
||||
# MUTABLE PROPERTIES
|
||||
iface = models.CharField(max_length=15, default=None, null=False) # e.g. en0
|
||||
hostname = models.CharField(max_length=63, default=None, null=False) # e.g. somehost.sub.example.com
|
||||
isp = models.CharField(max_length=63, default=None, null=False) # e.g. AS-SONICTELECOM
|
||||
city = models.CharField(max_length=63, default=None, null=False) # e.g. Berkeley
|
||||
region = models.CharField(max_length=63, default=None, null=False) # e.g. California
|
||||
country = models.CharField(max_length=63, default=None, null=False) # e.g. United States
|
||||
|
||||
objects = NetworkInterfaceManager()
|
||||
|
||||
class Meta:
|
||||
unique_together = (
|
||||
('machine', 'ip_public', 'ip_local', 'mac_address', 'dns_server'),
|
||||
)
|
||||
|
||||
|
||||
# class InstalledBinary(ABIDModel):
|
||||
# abid_prefix = 'bin_'
|
||||
# abid_ts_src = 'self.machine.created_at'
|
||||
# abid_uri_src = 'self.machine.guid'
|
||||
# abid_subtype_src = 'self.binprovider'
|
||||
# abid_rand_src = 'self.id'
|
||||
# abid_drift_allowed = False
|
||||
|
||||
# id = models.UUIDField(primary_key=True, default=None, null=False, editable=False, unique=True, verbose_name='ID')
|
||||
# abid = ABIDField(prefix=abid_prefix)
|
||||
|
||||
# created_at = AutoDateTimeField(default=None, null=False, db_index=True)
|
||||
# modified_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
# machine = models.ForeignKey(Machine, on_delete=models.CASCADE, default=None, null=False)
|
||||
# binprovider = models.CharField(max_length=255, default=None, null=False)
|
||||
|
||||
# name = models.CharField(max_length=255, default=None, null=False)
|
||||
# version = models.CharField(max_length=255, default=None, null=False)
|
||||
# abspath = models.CharField(max_length=255, default=None, null=False)
|
||||
# sha256 = models.CharField(max_length=255, default=None, null=False)
|
||||
|
||||
# class Meta:
|
||||
# unique_together = (
|
||||
# ('machine', 'binprovider', 'version', 'abspath', 'sha256'),
|
||||
# )
|
||||
Reference in New Issue
Block a user