diff --git a/src/aleph/vm/conf.py b/src/aleph/vm/conf.py index 640d765e3..940f5b17c 100644 --- a/src/aleph/vm/conf.py +++ b/src/aleph/vm/conf.py @@ -127,6 +127,11 @@ class Settings(BaseSettings): WATCH_FOR_UPDATES = True API_SERVER = "https://official.aleph.cloud" + # Connect to the Quad9 VPN provider using their IPv4 and IPv6 addresses. + CONNECTIVITY_IPV4_URL = "https://9.9.9.9/" + CONNECTIVITY_IPV6_URL = "https://[2620:fe::fe]/" + CONNECTIVITY_DNS_HOSTNAME = "example.org" + USE_JAILER = True # System logs make boot ~2x slower PRINT_SYSTEM_LOGS = False diff --git a/src/aleph/vm/orchestrator/supervisor.py b/src/aleph/vm/orchestrator/supervisor.py index ed94ff150..5297c9199 100644 --- a/src/aleph/vm/orchestrator/supervisor.py +++ b/src/aleph/vm/orchestrator/supervisor.py @@ -28,6 +28,7 @@ run_code_from_hostname, run_code_from_path, status_check_fastapi, + status_check_host, status_check_version, status_public_config, update_allocations, @@ -90,6 +91,7 @@ async def allow_cors_on_endpoint(request: web.Request): allow_cors_on_endpoint, ), web.get("/status/check/fastapi", status_check_fastapi), + web.get("/status/check/host", status_check_host), web.get("/status/check/version", status_check_version), web.get("/status/config", status_public_config), web.static("/static", Path(__file__).parent / "views/static"), diff --git a/src/aleph/vm/orchestrator/views/__init__.py b/src/aleph/vm/orchestrator/views/__init__.py index 106aa27e8..11daae50f 100644 --- a/src/aleph/vm/orchestrator/views/__init__.py +++ b/src/aleph/vm/orchestrator/views/__init__.py @@ -26,6 +26,14 @@ from aleph.vm.orchestrator.pubsub import PubSub from aleph.vm.orchestrator.resources import Allocation from aleph.vm.orchestrator.run import run_code_on_request, start_persistent_vm +from aleph.vm.orchestrator.views.host_status import ( + check_dns_ipv4, + check_dns_ipv6, + check_domain_resolution_ipv4, + check_domain_resolution_ipv6, + check_host_egress_ipv4, + check_host_egress_ipv6, +) from aleph.vm.pool import VmPool from aleph.vm.utils import ( HostNotFoundError, @@ -169,6 +177,25 @@ async def status_check_fastapi(request: web.Request): return web.json_response(result, status=200 if all(result.values()) else 503) +async def status_check_host(request: web.Request): + """Check that the platform is supported and configured correctly""" + + result = { + "ipv4": { + "egress": await check_host_egress_ipv4(), + "dns": await check_dns_ipv4(), + "domain": await check_domain_resolution_ipv4(), + }, + "ipv6": { + "egress": await check_host_egress_ipv6(), + "dns": await check_dns_ipv6(), + "domain": await check_domain_resolution_ipv6(), + }, + } + result_status = 200 if all(result["ipv4"].values()) and all(result["ipv6"].values()) else 503 + return web.json_response(result, status=result_status) + + async def status_check_version(request: web.Request): """Check if the software is running a version equal or newer than the given one""" reference_str: Optional[str] = request.query.get("reference") diff --git a/src/aleph/vm/orchestrator/views/host_status.py b/src/aleph/vm/orchestrator/views/host_status.py new file mode 100644 index 000000000..7bea32604 --- /dev/null +++ b/src/aleph/vm/orchestrator/views/host_status.py @@ -0,0 +1,85 @@ +import logging +import socket +from typing import Any, Awaitable, Callable, Tuple + +import aiohttp + +from aleph.vm.conf import settings + +logger = logging.getLogger(__name__) + + +def return_false_on_timeout(func: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[bool]]: + async def wrapper(*args: Any, **kwargs: Any) -> bool: + try: + return await func(*args, **kwargs) + except TimeoutError: + logger.warning(f"Timeout while checking {func.__name__}") + return False + + return wrapper + + +async def check_ip_connectivity(url: str, socket_family: socket.AddressFamily = socket.AF_UNSPEC) -> bool: + timeout = aiohttp.ClientTimeout(total=5) + async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(), timeout=timeout) as session: + async with session.get(url, socket_family=socket_family) as resp: + # We expect the Quad9 endpoints to return a 404 error, but other endpoints may return a 200 + if resp.status not in (200, 404): + resp.raise_for_status() + return True + + +@return_false_on_timeout +async def check_host_egress_ipv4() -> bool: + """Check if the host has IPv4 connectivity.""" + return await check_ip_connectivity(settings.CONNECTIVITY_IPV4_URL) + + +@return_false_on_timeout +async def check_host_egress_ipv6() -> bool: + """Check if the host has IPv6 connectivity.""" + return await check_ip_connectivity(settings.CONNECTIVITY_IPV6_URL) + + +async def resolve_dns(hostname: str) -> Tuple[str, str]: + info_inet, info_inet6 = socket.getaddrinfo(hostname, 80, proto=socket.IPPROTO_TCP) + ipv4 = info_inet[4][0] + ipv6 = info_inet6[4][0] + return ipv4, ipv6 + + +async def check_dns_ipv4() -> bool: + """Check if DNS resolution is working via IPv4.""" + ipv4, _ = await resolve_dns(settings.CONNECTIVITY_DNS_HOSTNAME) + return bool(ipv4) + + +async def check_dns_ipv6() -> bool: + """Check if DNS resolution is working via IPv6.""" + _, ipv6 = await resolve_dns(settings.CONNECTIVITY_DNS_HOSTNAME) + return bool(ipv6) + + +async def check_domain_resolution_ipv4() -> bool: + """Check if the host's hostname resolves to an IPv4 address.""" + ipv4, _ = await resolve_dns(settings.DOMAIN_NAME) + return bool(ipv4) + + +async def check_domain_resolution_ipv6() -> bool: + """Check if the host's hostname resolves to an IPv6 address.""" + _, ipv6 = await resolve_dns(settings.DOMAIN_NAME) + return bool(ipv6) + + +@return_false_on_timeout +async def check_domain_ipv4() -> bool: + """Check if the host's hostname is accessible via IPv4.""" + return await check_ip_connectivity(settings.DOMAIN_NAME, socket.AF_INET) + + +@return_false_on_timeout +async def check_domain_ipv6() -> bool: + """Check if the host's hostname is accessible via IPv6.""" + return await check_ip_connectivity(settings.DOMAIN_NAME, socket.AF_INET6) diff --git a/src/aleph/vm/orchestrator/views/static/helpers.js b/src/aleph/vm/orchestrator/views/static/helpers.js index 8335e2faf..46d12e4b6 100644 --- a/src/aleph/vm/orchestrator/views/static/helpers.js +++ b/src/aleph/vm/orchestrator/views/static/helpers.js @@ -1,4 +1,4 @@ -async function fetchApiStatus () { +async function fetchFastapiCheckStatus () { const q = await fetch('/status/check/fastapi'); let res = { status: q.status, @@ -12,8 +12,10 @@ async function fetchApiStatus () { case 503: res.status = " is not working properly ❌"; res.details = await q.json(); + break; case 500: res.status = " ❌ Failed"; + break; default: res.status = q.status; } @@ -22,6 +24,36 @@ async function fetchApiStatus () { return res; } +async function fetchHostCheckStatus () { + const q = await fetch('/status/check/host'); + let res = { + status: q.status, + details: [] + } + if(q.ok){ + res.status = " is working properly ✅"; + } + else { + switch(Number(q.status)){ + case 503: + res.status = " is not working properly ❌"; + res.details = await q.json(); + break; + case 500: + res.status = " ❌ Failed"; + break; + default: + res.status = q.status; + } + } + + return res; +} + +function objectToString (obj) { + return Object.entries(obj).reduce((acc, [k, v]) => acc + `
  • ${k}: ${v}
  • \n`, ''); +} + const buildQueryParams = (params) => Object.entries(params).reduce((acc, [k, v]) => acc + `${k}=${v}&`, '?').slice(0, -1); const isLatestRelease = async () => { diff --git a/src/aleph/vm/orchestrator/views/static/main.css b/src/aleph/vm/orchestrator/views/static/main.css index 1c14ddd63..bf2cbbf85 100644 --- a/src/aleph/vm/orchestrator/views/static/main.css +++ b/src/aleph/vm/orchestrator/views/static/main.css @@ -1,11 +1,14 @@ body { font-family: IBM Plex Regular, monospace; + white-space: normal; margin: auto; + max-width: 800px; } main { width: 90vw; margin: 2vh auto; + max-width: 800px; } progress { @@ -36,29 +39,29 @@ progress { @keyframes move { 0% { - height: 20px; + height: 10px; } 50% { - height: 10px; + height: 5px; } 100% { - height: 20px; + height: 10px; } } @keyframes move2 { 0% { - height: 10px; + height: 5px; } 50% { - height: 20px; + height: 10px; } 100% { - height: 10px; + height: 5px; } } @@ -97,4 +100,4 @@ progress { footer{ font-size: 70%; opacity: .75; -} \ No newline at end of file +} diff --git a/src/aleph/vm/orchestrator/views/templates/index.html b/src/aleph/vm/orchestrator/views/templates/index.html index 6e1cc5a0e..d7b449f21 100644 --- a/src/aleph/vm/orchestrator/views/templates/index.html +++ b/src/aleph/vm/orchestrator/views/templates/index.html @@ -16,10 +16,10 @@

    Aleph.im Compute Node

    This is an Aleph.im compute resource node.

    - It executes user programs stored on the Aleph network in Virtual Machines. + It executes user programs stored on the aleph.im network in Virtual Machines.

    - See the repository for more info. + See the source code repository for more info.

    @@ -44,34 +44,109 @@

    Multiaddr

    Diagnostic

    -

    - Virtualization  - - ...  - - - - +

    +

    Virtualization

    +

    + Virtualization + + ... + + + + + - -

    - -
    
    -        

    - Diagnostics checks | - Open diagnostic VM -

    -

    - Egress IPv6 - - is ... - - - - +

    +
    +
      +
      + +
      + +
      +

      Host connectivity

      +

      + Host + + ... + + + + + - -

      +

      +
      +

      IPv4

      +
        +

        IPv6

        +
          +
          + +
          + +
          + ℹ️ More information + +
          +

          Latest metrics

          +

          + The aleph.im network measures the performance of all nodes in the network. New metrics are published + every 10 minutes. +

          +

          + 🔍 Browse the metrics in the explorer +

          +
            +
            + +

            VM Egress IPv6

            +

            + VM Egress IPv6 is a test to check if virtual machines are able to connect to the IPv6 internet. + Enabling VM IPv6 Egress requires a specific configuration that is not applied automatically. It is not yet + required to run virtual machines. +

            +
            +

            + VM Egress IPv6 + + is ... + + + + + + +

            +
            +

            APIs

            +

            + Host status check API: /status/check/host +

            +

            + + Virtualization check API: /status/check/fastapi +

            +

            + + VM Egress IPv6:
            + /vm/$check_fastapi_vm_id/ip/6 +

            +
            +
            @@ -80,7 +155,7 @@

            Version

            Running version $version.

            -

            +

            @@ -121,13 +196,26 @@

            Version

            (async () => { try { - const { status, details } = await fetchApiStatus(); - document.getElementById('check').innerHTML = status; - const _checksDiv = document.getElementById("checks"); - if(details.length > 0){ - const detailsDiv = document.createElement('div'); - detailsDiv.innerHTML = details; - _checksDiv.appendChild(detailsDiv); + const { status, details } = await fetchFastapiCheckStatus(); + document.getElementById('virtualization-check').innerHTML = status; + if(Object.keys(details).length > 0){ + const detailsDiv = document.querySelector("#virtualization-checks .details ul"); + detailsDiv.innerHTML = objectToString(details); + document.querySelector("#virtualization-checks .help").style.display = "block"; + } + } catch (err) { + console.error('Could not fetch api status', err); + } + })(); + + (async () => { + try { + const { status, details } = await fetchHostCheckStatus(); + document.getElementById('host-check').innerHTML = status; + if(Object.keys(details).length > 0){ + document.querySelector("#host-checks .details ul.ipv4").innerHTML = objectToString(details["ipv4"]); + document.querySelector("#host-checks .details ul.ipv6").innerHTML = objectToString(details["ipv6"]); + document.querySelector("#host-checks .help").style.display = "block"; } } catch (err) { console.error('Could not fetch api status', err); @@ -238,21 +326,21 @@

            Version

            try{ const response = await fetch('/vm/$check_fastapi_vm_id/ip/6'); if (response.ok) { - document.getElementById("check_ipv6").innerHTML = "is working ✔️"; + document.getElementById("ipv6-egress-check").innerHTML = "is working ✔️"; } else if (response.status === 503) { - document.getElementById("check_ipv6").innerHTML = "fails to be tested ❌ "; + document.getElementById("ipv6-egress-check").innerHTML = "fails to be tested ❌ "; } else if (response.status === 500) { - document.getElementById("check_ipv6").innerHTML = "is not available ⛌"; + document.getElementById("ipv6-egress-check").innerHTML = "is not yet available ⛌"; } else { - document.getElementById("check_ipv6").innerText = response.status; + document.getElementById("ipv6-egress-check").innerText = response.status; } } catch(err){ console.error(err); - document.getElementById("check_ipv6").innerHTML = "fails to be tested ❌ "; + document.getElementById("ipv6-egress-check").innerHTML = "fails to be tested ❌ "; } })();