Skip to content

Commit 5a01c42

Browse files
authored
Fix: Diagnostic API was not updated
We published multiple changes to the diagnostic VM recently but none of these was released. This provides a new diagnostic VM, based on a new runtime [1], with fixes: - Reading messages with the newer SDK - Better handling of IPv6 detection errors - Two different tests for signing messages (local and remote) - aleph-message version was not specified - fetching a single message was not tested
1 parent 84614a5 commit 5a01c42

File tree

8 files changed

+224
-41
lines changed

8 files changed

+224
-41
lines changed

.github/workflows/test-on-droplets-matrix.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,11 @@ jobs:
134134
- alias: "runtime-6770" # Old runtime, using Debian 11
135135
item_hash: "67705389842a0a1b95eaa408b009741027964edc805997475e95c505d642edd8"
136136
query_params: "?retro-compatibility=true"
137-
- alias: "runtime-3fc0" # New runtime, using Debian 12
137+
- alias: "runtime-3fc0" # Newer runtime, using Debian 12 but now old SDK
138138
item_hash: "3fc0aa9569da840c43e7bd2033c3c580abb46b007527d6d20f2d4e98e867f7af"
139+
query_params: "?retro-compatibility=true"
140+
- alias: "runtime-63fa" # Latest runtime, using Debian 12 and SDK 0.9.0
141+
item_hash: "63faf8b5db1cf8d965e6a464a0cb8062af8e7df131729e48738342d956f29ace"
139142
query_params: ""
140143

141144
steps:

examples/example_fastapi/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Publish using:
2+
3+
```shell
4+
aleph program upload ../aleph-vm/examples/example_fastapi main:app \
5+
--persistent-volume "persistence=host,size_mib=1,mount=/var/lib/example,name=increment-storage,comment=Persistence"
6+
```

examples/example_fastapi/main.py

Lines changed: 153 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,30 @@
55
import socket
66
import subprocess
77
import sys
8-
from datetime import datetime
8+
from datetime import datetime, timezone
99
from os import listdir
1010
from pathlib import Path
11-
from typing import List, Optional
11+
from typing import Any, Optional
1212

1313
import aiohttp
14+
from aleph_message.models import (
15+
MessagesResponse,
16+
PostMessage,
17+
ProgramMessage,
18+
StoreMessage,
19+
)
20+
from aleph_message.status import MessageStatus
1421
from fastapi import FastAPI
1522
from fastapi.middleware.cors import CORSMiddleware
1623
from fastapi.responses import PlainTextResponse
1724
from pip._internal.operations.freeze import freeze
1825
from pydantic import BaseModel, HttpUrl
1926
from starlette.responses import JSONResponse
2027

28+
from aleph.sdk.chains.ethereum import get_fallback_account
2129
from aleph.sdk.chains.remote import RemoteAccount
22-
from aleph.sdk.client import AlephClient, AuthenticatedAlephClient
30+
from aleph.sdk.client import AlephHttpClient, AuthenticatedAlephHttpClient
31+
from aleph.sdk.query.filters import MessageFilter
2332
from aleph.sdk.types import StorageEnum
2433
from aleph.sdk.vm.app import AlephApp
2534
from aleph.sdk.vm.cache import VmCache
@@ -42,30 +51,47 @@
4251

4352

4453
@app.on_event("startup")
45-
async def startup_event():
54+
async def startup_event() -> None:
4655
global startup_lifespan_executed
4756
startup_lifespan_executed = True
4857

4958

5059
@app.get("/")
51-
async def index():
60+
async def index() -> dict[str, Any]:
5261
if os.path.exists("/opt/venv"):
5362
opt_venv = list(listdir("/opt/venv"))
5463
else:
5564
opt_venv = []
5665
return {
5766
"Example": "example_fastapi",
5867
"endpoints": [
68+
# Features
69+
"/lifespan",
5970
"/environ",
60-
"/messages",
71+
"/state/increment",
72+
"/wait-for/{delay}",
73+
# Local cache
74+
"/cache/get/{key}",
75+
"/cache/set/{key}/{value}",
76+
"/cache/remove/{key}",
77+
"/cache/keys",
78+
# Networking
6179
"/dns",
62-
"ip/address",
80+
"/ip/address",
6381
"/ip/4",
6482
"/ip/6",
6583
"/internet",
84+
# Error handling
85+
"/raise",
86+
"/crash",
87+
# Aleph.im
88+
"/messages",
89+
"/get_a_message",
6690
"/post_a_message",
67-
"/state/increment",
68-
"/wait-for/{delay}",
91+
"/post_a_message_local_account",
92+
"/post_a_file",
93+
"/sign_a_message",
94+
# Platform properties
6995
"/platform/os",
7096
"/platform/python",
7197
"/platform/pip-freeze",
@@ -91,10 +117,11 @@ async def environ() -> dict[str, str]:
91117

92118

93119
@app.get("/messages")
94-
async def read_aleph_messages():
120+
async def read_aleph_messages() -> dict[str, MessagesResponse]:
95121
"""Read data from Aleph using the Aleph Client library."""
96-
async with AlephClient() as client:
97-
data = await client.get_messages(hashes=["f246f873c3e0f637a15c566e7a465d2ecbb83eaa024d54ccb8fb566b549a929e"])
122+
async with AlephHttpClient() as client:
123+
message_filter = MessageFilter(hashes=["f246f873c3e0f637a15c566e7a465d2ecbb83eaa024d54ccb8fb566b549a929e"])
124+
data = await client.get_messages(message_filter=message_filter)
98125
return {"Messages": data}
99126

100127

@@ -163,9 +190,13 @@ async def connect_ipv6():
163190
if resp.status != 404:
164191
resp.raise_for_status()
165192
return {"result": True, "headers": resp.headers}
166-
except aiohttp.ClientTimeout:
167-
logger.warning(f"Session connection for host {ipv6_host} failed")
168-
return {"result": False, "headers": resp.headers}
193+
except TimeoutError:
194+
logger.warning(f"Session connection to host {ipv6_host} timed out")
195+
return {"result": False, "reason": "Timeout"}
196+
except aiohttp.ClientConnectionError as error:
197+
logger.warning(f"Client connection to host {ipv6_host} failed: {error}")
198+
# Get a string that describes the error
199+
return {"result": False, "reason": str(error.args[0])}
169200

170201

171202
async def check_url(internet_host: HttpUrl, timeout_seconds: int = 5):
@@ -184,15 +215,15 @@ async def check_url(internet_host: HttpUrl, timeout_seconds: int = 5):
184215
@app.get("/internet")
185216
async def read_internet():
186217
"""Check Internet connectivity of the system, requiring IP connectivity, domain resolution and HTTPS/TLS."""
187-
internet_hosts: List[HttpUrl] = [
218+
internet_hosts: list[HttpUrl] = [
188219
HttpUrl(url="https://aleph.im/", scheme="https"),
189220
HttpUrl(url="https://ethereum.org", scheme="https"),
190221
HttpUrl(url="https://ipfs.io/", scheme="https"),
191222
]
192223
timeout_seconds = 5
193224

194225
# Create a list of tasks to check the URLs in parallel
195-
tasks: set[asyncio.Task] = set(asyncio.create_task(check_url(host, timeout_seconds)) for host in internet_hosts)
226+
tasks: set[asyncio.Task] = {asyncio.create_task(check_url(host, timeout_seconds)) for host in internet_hosts}
196227

197228
# While no tasks have completed, keep waiting for the next one to finish
198229
while tasks:
@@ -211,34 +242,121 @@ async def read_internet():
211242
return {"result": False}
212243

213244

214-
@app.get("/post_a_message")
215-
async def post_a_message():
216-
"""Post a message on the Aleph network"""
245+
@app.get("/get_a_message")
246+
async def get_a_message():
247+
"""Get a message from the Aleph.im network"""
248+
item_hash = "3fc0aa9569da840c43e7bd2033c3c580abb46b007527d6d20f2d4e98e867f7af"
249+
async with AlephHttpClient() as client:
250+
message = await client.get_message(
251+
item_hash=item_hash,
252+
message_type=ProgramMessage,
253+
)
254+
return message.dict()
217255

218-
account = await RemoteAccount.from_crypto_host(host="http://localhost", unix_socket="/tmp/socat-socket")
256+
257+
@app.post("/post_a_message")
258+
async def post_with_remote_account():
259+
"""Post a message on the Aleph.im network using the remote account of the host."""
260+
try:
261+
account = await RemoteAccount.from_crypto_host(host="http://localhost", unix_socket="/tmp/socat-socket")
262+
263+
content = {
264+
"date": datetime.now(tz=timezone.utc).isoformat(),
265+
"test": True,
266+
"answer": 42,
267+
"something": "interesting",
268+
}
269+
async with AuthenticatedAlephHttpClient(
270+
account=account,
271+
) as client:
272+
message: PostMessage
273+
status: MessageStatus
274+
message, status = await client.create_post(
275+
post_content=content,
276+
post_type="test",
277+
ref=None,
278+
channel="TEST",
279+
inline=True,
280+
storage_engine=StorageEnum.storage,
281+
sync=True,
282+
)
283+
if status != MessageStatus.PROCESSED:
284+
return JSONResponse(status_code=500, content={"error": status})
285+
return {
286+
"message": message,
287+
}
288+
except aiohttp.client_exceptions.UnixClientConnectorError:
289+
return JSONResponse(status_code=500, content={"error": "Could not connect to the remote account"})
290+
291+
292+
@app.post("/post_a_message_local_account")
293+
async def post_with_local_account():
294+
"""Post a message on the Aleph.im network using a local private key."""
295+
296+
account = get_fallback_account()
219297

220298
content = {
221-
"date": datetime.utcnow().isoformat(),
299+
"date": datetime.now(tz=timezone.utc).isoformat(),
222300
"test": True,
223301
"answer": 42,
224302
"something": "interesting",
225303
}
226-
async with AuthenticatedAlephClient(
304+
async with AuthenticatedAlephHttpClient(
227305
account=account,
306+
api_server="https://api2.aleph.im",
307+
allow_unix_sockets=False,
228308
) as client:
229-
response = await client.create_post(
309+
message: PostMessage
310+
status: MessageStatus
311+
message, status = await client.create_post(
230312
post_content=content,
231313
post_type="test",
232314
ref=None,
233315
channel="TEST",
234316
inline=True,
235317
storage_engine=StorageEnum.storage,
318+
sync=True,
319+
)
320+
if status != MessageStatus.PROCESSED:
321+
return JSONResponse(status_code=500, content={"error": status})
322+
return {
323+
"message": message,
324+
}
325+
326+
327+
@app.post("/post_a_file")
328+
async def post_a_file():
329+
account = get_fallback_account()
330+
file_path = Path(__file__).absolute()
331+
async with AuthenticatedAlephHttpClient(
332+
account=account,
333+
) as client:
334+
message: StoreMessage
335+
status: MessageStatus
336+
message, status = await client.create_store(
337+
file_path=file_path,
338+
ref=None,
339+
channel="TEST",
340+
storage_engine=StorageEnum.storage,
341+
sync=True,
236342
)
343+
if status != MessageStatus.PROCESSED:
344+
return JSONResponse(status_code=500, content={"error": status})
237345
return {
238-
"response": response,
346+
"message": message,
239347
}
240348

241349

350+
@app.get("/sign_a_message")
351+
async def sign_a_message():
352+
"""Sign a message using a locally managed account within the virtual machine."""
353+
# FIXME: Broken, fixing this depends on https://github.com/aleph-im/aleph-sdk-python/pull/120
354+
account = get_fallback_account()
355+
message = {"hello": "world", "chain": "ETH"}
356+
signed_message = await account.sign_message(message)
357+
return {"message": signed_message}
358+
359+
242360
@app.get("/cache/get/{key}")
243361
async def get_from_cache(key: str):
244362
"""Get data in the VM cache"""
@@ -265,7 +383,7 @@ async def keys_from_cache(pattern: str = "*"):
265383

266384

267385
@app.get("/state/increment")
268-
async def increment():
386+
async def increment() -> dict[str, int]:
269387
path = "/var/lib/example/storage.json"
270388
try:
271389
with open(path) as fd:
@@ -284,7 +402,7 @@ class Data(BaseModel):
284402

285403

286404
@app.post("/post")
287-
async def receive_post(data: Data):
405+
async def receive_post(data: Data) -> str:
288406
return str(data)
289407

290408

@@ -293,13 +411,14 @@ class CustomError(Exception):
293411

294412

295413
@app.get("/raise")
296-
def raise_error():
414+
def raise_error() -> None:
297415
"""Raises an error to check that the init handles it properly without crashing"""
298-
raise CustomError("Whoops")
416+
error_message = "Whoops"
417+
raise CustomError(error_message)
299418

300419

301420
@app.get("/crash")
302-
def crash():
421+
def crash() -> None:
303422
"""Crash the entire VM in order to check that the supervisor can handle it"""
304423
sys.exit(1)
305424

@@ -313,22 +432,22 @@ def crash():
313432

314433

315434
@app.get("/platform/os")
316-
def platform_os():
435+
def platform_os() -> PlainTextResponse:
317436
return PlainTextResponse(content=Path("/etc/os-release").read_text())
318437

319438

320439
@app.get("/platform/python")
321-
def platform_python():
440+
def platform_python() -> PlainTextResponse:
322441
return PlainTextResponse(content=sys.version)
323442

324443

325444
@app.get("/platform/pip-freeze")
326-
def platform_pip_freeze():
445+
def platform_pip_freeze() -> list[str]:
327446
return list(freeze())
328447

329448

330449
@app.event(filters=filters)
331-
async def aleph_event(event):
450+
async def aleph_event(event) -> dict[str, str]:
332451
print("aleph_event", event)
333452
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector()) as session:
334453
async with session.get("https://official.aleph.cloud/api/v0/info/public.json") as resp:

runtimes/aleph-debian-12-python/create_disk_image.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ locale-gen en_US.UTF-8
3636
3737
echo "Pip installing aleph-sdk-python"
3838
mkdir -p /opt/aleph/libs
39-
pip3 install --target /opt/aleph/libs 'aleph-sdk-python==0.9.0' 'fastapi~=0.109.2'
39+
pip3 install --target /opt/aleph/libs 'aleph-sdk-python==0.9.0' 'aleph-message==0.4.4' 'fastapi~=0.109.2'
4040
4141
# Compile Python code to bytecode for faster execution
4242
# -o2 is needed to compile with optimization level 2 which is what we launch init1.py (`python -OO`)

src/aleph/vm/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ class Settings(BaseSettings):
289289
)
290290
FAKE_INSTANCE_MESSAGE = Path(abspath(join(__file__, "../../../../examples/instance_message_from_aleph.json")))
291291

292-
CHECK_FASTAPI_VM_ID = "3fc0aa9569da840c43e7bd2033c3c580abb46b007527d6d20f2d4e98e867f7af"
292+
CHECK_FASTAPI_VM_ID = "63faf8b5db1cf8d965e6a464a0cb8062af8e7df131729e48738342d956f29ace"
293293
LEGACY_CHECK_FASTAPI_VM_ID = "67705389842a0a1b95eaa408b009741027964edc805997475e95c505d642edd8"
294294

295295
# Developer options

src/aleph/vm/orchestrator/run.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ async def build_asgi_scope(path: str, request: web.Request) -> dict[str, Any]:
4444

4545

4646
async def build_event_scope(event) -> dict[str, Any]:
47+
"""Build an ASGI scope for an event."""
4748
return {
4849
"type": "aleph.message",
4950
"body": event,

0 commit comments

Comments
 (0)