Skip to content

Commit

Permalink
Add terms and conditions (updated)
Browse files Browse the repository at this point in the history
  • Loading branch information
philogicae committed Dec 13, 2024
1 parent af117b9 commit c6a1f28
Show file tree
Hide file tree
Showing 11 changed files with 141 additions and 42 deletions.
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ dependencies = [
"aiodns==3.2",
"aiohttp==3.10.6",
"aleph-message>=0.6",
"aleph-sdk-python>=1.2.1,<2",
"base58==2.1.1", # Needed now as default with _load_account changement
"py-sr25519-bindings==0.2", # Needed for DOT signatures
"aleph-sdk-python @ git+https://github.com/aleph-im/aleph-sdk-python@feat-add-ipfs-gateway",
"base58==2.1.1", # Needed now as default with _load_account changement
"py-sr25519-bindings==0.2", # Needed for DOT signatures
"pygments==2.18",
"pynacl==1.5", # Needed now as default with _load_account changement
"pynacl==1.5", # Needed now as default with _load_account changement
"python-magic==0.4.27",
"rich==13.9.3",
"setuptools>=65.5",
"substrate-interface==1.7.11", # Needed for DOT signatures
"substrate-interface==1.7.11", # Needed for DOT signatures
"textual==0.73",
"typer==0.12.5",
]
Expand Down
7 changes: 4 additions & 3 deletions scripts/gendoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
Copied from typer.cli.py to customise doc generation
"""

import click
import importlib.util
import re
import sys
from pathlib import Path
from typing import Any, List, Optional

import click
import typer
import typer.core
from click import Command, Group
from pathlib import Path
from typing import Any, List, Optional

default_app_names = ("app", "cli", "main")
default_func_names = ("main", "cli", "app")
Expand Down
48 changes: 32 additions & 16 deletions src/aleph_client/commands/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
from aleph.sdk import AlephHttpClient, AuthenticatedAlephHttpClient
from aleph.sdk.account import _load_account
from aleph.sdk.conf import settings
from aleph.sdk.types import AccountFromPrivateKey, StorageEnum
from aleph_message.models import ItemHash, StoreMessage
from aleph.sdk.exceptions import ForgottenMessageError, MessageNotFoundError
from aleph.sdk.types import AccountFromPrivateKey, StorageEnum, StoredContent
from aleph.sdk.utils import safe_getattr
from aleph_message.models import ItemHash, ItemType, MessageType, StoreMessage
from aleph_message.status import MessageStatus
from pydantic import BaseModel, Field
from rich import box
Expand Down Expand Up @@ -101,28 +103,42 @@ async def download(
output_path: Path = typer.Option(Path("."), help="Output directory path"),
file_name: str = typer.Option(None, help="Output file name (without extension)"),
file_extension: str = typer.Option(None, help="Output file extension"),
only_info: bool = False,
verbose: bool = True,
debug: bool = False,
):
"""Download a file on aleph.im."""
) -> Optional[StoredContent]:
"""Download a file on aleph.im or display related infos."""

setup_logging(debug)

output_path.mkdir(parents=True, exist_ok=True)
if not only_info:
output_path.mkdir(parents=True, exist_ok=True)

file_name = file_name if file_name else hash
file_extension = file_extension if file_extension else ""
file_name = file_name if file_name else hash
file_extension = file_extension if file_extension else ""

output_file_path = output_path / f"{file_name}{file_extension}"
output_file_path = output_path / f"{file_name}{file_extension}"

async with AlephHttpClient(api_server=settings.API_HOST) as client:
logger.info(f"Downloading {hash} ...")
with open(output_file_path, "wb") as fd:
if not use_ipfs:
await client.download_file_to_buffer(hash, fd)
else:
await client.download_file_ipfs_to_buffer(hash, fd)
async with AlephHttpClient(api_server=settings.API_HOST) as client:
logger.info(f"Downloading {hash} ...")
with open(output_file_path, "wb") as fd:
if not use_ipfs:
await client.download_file_to_buffer(hash, fd)
else:
await client.download_file_ipfs_to_buffer(hash, fd)

Check warning on line 128 in src/aleph_client/commands/files.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/files.py#L128

Added line #L128 was not covered by tests

logger.debug("File downloaded successfully.")
logger.debug("File downloaded successfully.")
else:
async with AlephHttpClient(api_server=settings.API_HOST) as client:
content = await client.get_stored_content(hash)
if verbose:
typer.echo(
f"Filename: {content.filename}\nHash: {content.hash}\nURL: {content.url}"
if safe_getattr(content, "url")
else safe_getattr(content, "error")
)
return content
return None


@app.command()
Expand Down
1 change: 1 addition & 0 deletions src/aleph_client/commands/help_strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
SSH_PUBKEY_FILE = "Path to a public ssh key to be added to the instance"
CRN_HASH = "Hash of the CRN to deploy to (only applicable for confidential and/or Pay-As-You-Go instances)"
CRN_URL = "URL of the CRN to deploy to (only applicable for confidential and/or Pay-As-You-Go instances)"
CRN_AUTO_TAC = "Automatically accept the Terms & Conditions of the CRN if you read them beforehand"
CONFIDENTIAL_OPTION = "Launch a confidential instance (requires creating an encrypted volume)"
CONFIDENTIAL_FIRMWARE = "Hash to UEFI Firmware to launch confidential instance"
CONFIDENTIAL_FIRMWARE_HASH = "Hash of the UEFI Firmware content, to validate measure (ignored if path is provided)"
Expand Down
36 changes: 33 additions & 3 deletions src/aleph_client/commands/instance/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from aleph.sdk.query.filters import MessageFilter
from aleph.sdk.query.responses import PriceResponse
from aleph.sdk.types import StorageEnum
from aleph.sdk.utils import calculate_firmware_hash
from aleph.sdk.utils import calculate_firmware_hash, safe_getattr
from aleph_message.models import InstanceMessage, StoreMessage
from aleph_message.models.base import Chain, MessageType
from aleph_message.models.execution.base import Payment, PaymentType
Expand Down Expand Up @@ -58,7 +58,6 @@
from aleph_client.commands.utils import (
filter_only_valid_messages,
get_or_prompt_volumes,
safe_getattr,
setup_logging,
str_to_datetime,
validate_ssh_pubkey_file,
Expand Down Expand Up @@ -117,6 +116,7 @@ async def create(
None,
help=help_strings.IMMUTABLE_VOLUME,
),
crn_auto_tac: bool = typer.Option(False, help=help_strings.CRN_AUTO_TAC),
channel: Optional[str] = typer.Option(default=settings.DEFAULT_CHANNEL, help=help_strings.CHANNEL),
private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY),
private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE),
Expand Down Expand Up @@ -342,6 +342,7 @@ async def create(
crn_info.get("computing", {}).get("ENABLE_CONFIDENTIAL_COMPUTING", False)
),
gpu_support=bool(crn_info.get("computing", {}).get("ENABLE_GPU_SUPPORT", False)),
terms_and_conditions=crn_info.get("terms_and_conditions"),
)
echo("\n* Selected CRN *")
crn.display_crn_specs()
Expand Down Expand Up @@ -413,6 +414,15 @@ async def create(
device_id=selected_gpu.device_id,
)
]
if crn.terms_and_conditions:
accepted = await crn.display_terms_and_conditions(auto_accept=crn_auto_tac)

Check warning on line 418 in src/aleph_client/commands/instance/__init__.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/instance/__init__.py#L418

Added line #L418 was not covered by tests
if accepted is None:
echo("Failed to fetch terms and conditions.\nContact support or use a different CRN.")
raise typer.Exit(1)

Check warning on line 421 in src/aleph_client/commands/instance/__init__.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/instance/__init__.py#L420-L421

Added lines #L420 - L421 were not covered by tests
elif not accepted:
echo("Terms & Conditions rejected: instance creation aborted.")
raise typer.Exit(1)
echo("Terms & Conditions accepted.")

Check warning on line 425 in src/aleph_client/commands/instance/__init__.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/instance/__init__.py#L423-L425

Added lines #L423 - L425 were not covered by tests

async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client:
payment = Payment(
Expand All @@ -437,7 +447,12 @@ async def create(
payment=payment,
requirements=(
HostRequirements(
node=NodeRequirements(node_hash=crn.hash),
node=NodeRequirements(
node_hash=crn.hash,
terms_and_conditions=(
ItemHash(crn.terms_and_conditions) if crn.terms_and_conditions else None
),
),
gpu=gpu_requirement,
)
if crn
Expand Down Expand Up @@ -734,6 +749,19 @@ async def _show_instances(messages: List[InstanceMessage], node_list: NodeInfo):
Text(str(info["ipv6_logs"])),
style="bright_yellow" if len(str(info["ipv6_logs"]).split(":")) == 8 else "dark_orange",
),
(
Text.assemble(
Text(
f"\n[{'✅' if info['terms_and_conditions']['accepted'] else '❌'}] Accepted Terms & Conditions:\n"
),
Text(
f"{info['terms_and_conditions']['url']}",
style="orange1",
),
)
if info["terms_and_conditions"]["hash"]
else ""
),
)
table.add_row(instance, specifications, status_column)
table.add_section()
Expand Down Expand Up @@ -1197,6 +1225,7 @@ async def confidential_create(
None,
help=help_strings.IMMUTABLE_VOLUME,
),
crn_auto_tac: bool = typer.Option(False, help=help_strings.CRN_AUTO_TAC),
channel: Optional[str] = typer.Option(default=settings.DEFAULT_CHANNEL, help=help_strings.CHANNEL),
private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY),
private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE),
Expand Down Expand Up @@ -1228,6 +1257,7 @@ async def confidential_create(
ssh_pubkey_file=ssh_pubkey_file,
crn_hash=crn_hash,
crn_url=crn_url,
crn_auto_tac=crn_auto_tac,
confidential=True,
confidential_firmware=confidential_firmware,
gpu=gpu,
Expand Down
6 changes: 6 additions & 0 deletions src/aleph_client/commands/instance/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def compose(self):
self.table.add_column("Free RAM 🌡", key="ram")
self.table.add_column("Free Disk 💿", key="hdd")
self.table.add_column("URL", key="url")
self.table.add_column("Terms & Conditions 📝", key="tac")

Check warning on line 75 in src/aleph_client/commands/instance/display.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/instance/display.py#L75

Added line #L75 was not covered by tests
yield Label("Choose a Compute Resource Node (CRN) to run your instance")
with Horizontal():
self.loader_label_start = Label(self.label_start)
Expand Down Expand Up @@ -103,6 +104,7 @@ async def fetch_node_list(self):
qemu_support=None,
confidential_computing=None,
gpu_support=None,
terms_and_conditions=node.get("terms_and_conditions"),
)

# Initialize the progress bar
Expand Down Expand Up @@ -161,6 +163,9 @@ async def fetch_node_info(self, node: CRNInfo):
return
self.filtered_crns += 1

# Fetch terms and conditions
tac = await node.terms_and_conditions_content

Check warning on line 167 in src/aleph_client/commands/instance/display.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/instance/display.py#L167

Added line #L167 was not covered by tests

self.table.add_row(
_format_score(node.score),
node.name,
Expand All @@ -173,6 +178,7 @@ async def fetch_node_info(self, node: CRNInfo):
node.display_ram,
node.display_hdd,
node.url,
tac.url if tac else "✖",
key=node.hash,
)

Expand Down
18 changes: 14 additions & 4 deletions src/aleph_client/commands/instance/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@
import logging
from ipaddress import IPv6Interface
from json import JSONDecodeError
from typing import Optional
from typing import Any, Optional

import aiohttp
from aleph.sdk import AlephHttpClient
from aleph.sdk.conf import settings
from aleph.sdk.utils import safe_getattr
from aleph_message.models import InstanceMessage
from aleph_message.models.execution.base import PaymentType
from aleph_message.models.item_hash import ItemHash
from pydantic import ValidationError

from aleph_client.commands import help_strings
from aleph_client.commands.files import download
from aleph_client.commands.node import NodeInfo, _fetch_nodes
from aleph_client.commands.utils import safe_getattr
from aleph_client.models import MachineUsage
from aleph_client.utils import AsyncTyper, fetch_json, sanitize_url
from aleph_client.utils import fetch_json, sanitize_url

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -68,7 +69,7 @@ async def fetch_crn_info(node_url: str) -> dict | None:
return None


async def fetch_vm_info(message: InstanceMessage, node_list: NodeInfo) -> tuple[str, dict[str, object]]:
async def fetch_vm_info(message: InstanceMessage, node_list: NodeInfo) -> tuple[str, dict[str, Any]]:
"""
Fetches VM information given an instance message and the node list.
Expand All @@ -93,6 +94,7 @@ async def fetch_vm_info(message: InstanceMessage, node_list: NodeInfo) -> tuple[
allocation_type="",
ipv6_logs="",
crn_url="",
terms_and_conditions=dict(hash=None, url=None, accepted=False),
)
try:
# Fetch from the scheduler API directly if no payment or no receiver (hold-tier non-confidential)
Expand All @@ -118,6 +120,14 @@ async def fetch_vm_info(message: InstanceMessage, node_list: NodeInfo) -> tuple[
for node in node_list.nodes:
if node["hash"] == safe_getattr(message, "content.requirements.node.node_hash"):
info["crn_url"] = node["address"].rstrip("/")

# Terms and conditions
tac_item_hash = safe_getattr(message, "content.requirements.node.terms_and_conditions")

Check warning on line 125 in src/aleph_client/commands/instance/network.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/instance/network.py#L125

Added line #L125 was not covered by tests
if tac_item_hash:
tac = await download(tac_item_hash, only_info=True, verbose=False)
tac_url = safe_getattr(tac, "url") or f"missing → {tac_item_hash}"
info["terms_and_conditions"] = dict(hash=tac_item_hash, url=tac_url, accepted=True)

Check warning on line 129 in src/aleph_client/commands/instance/network.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/instance/network.py#L127-L129

Added lines #L127 - L129 were not covered by tests

path = f"{node['address'].rstrip('/')}/about/executions/list"
executions = await fetch_json(session, path)
if message.item_hash in executions:
Expand Down
8 changes: 0 additions & 8 deletions src/aleph_client/commands/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,14 +239,6 @@ def is_environment_interactive() -> bool:
)


def safe_getattr(obj, attr, default=None):
for part in attr.split("."):
obj = getattr(obj, part, default)
if obj is default:
break
return obj


async def wait_for_processed_instance(session: ClientSession, item_hash: ItemHash):
"""Wait for a message to be processed by CCN"""
while True:
Expand Down
28 changes: 27 additions & 1 deletion src/aleph_client/models.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from datetime import datetime
from enum import Enum
from typing import List, Optional

from aleph.sdk.types import StoredContent
from aleph_message.models import ItemHash
from aleph_message.models.execution.environment import CpuProperties
from pydantic import BaseModel
from rich.prompt import Prompt
from typer import echo

from aleph_client.commands.files import download
from aleph_client.commands.node import _escape_and_normalize, _remove_ansi_escape


Expand Down Expand Up @@ -130,6 +132,7 @@ class CRNInfo(BaseModel):
qemu_support: Optional[bool]
confidential_computing: Optional[bool]
gpu_support: Optional[bool]
terms_and_conditions: Optional[str]

@property
def display_cpu(self) -> str:
Expand All @@ -149,6 +152,27 @@ def display_hdd(self) -> str:
return f"{self.machine_usage.disk.available_kB / 1_000_000:>4.0f} / {self.machine_usage.disk.total_kB / 1_000_000:>4.0f} GB"
return ""

@property
async def terms_and_conditions_content(self) -> Optional[StoredContent]:
if self.terms_and_conditions:
return await download(self.terms_and_conditions, only_info=True, verbose=False)
return None

Check warning on line 159 in src/aleph_client/models.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/models.py#L158-L159

Added lines #L158 - L159 were not covered by tests

async def display_terms_and_conditions(self, auto_accept: bool = False) -> Optional[bool]:
if self.terms_and_conditions:
tac = await self.terms_and_conditions_content
echo("* Terms & Conditions *")

Check warning on line 164 in src/aleph_client/models.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/models.py#L163-L164

Added lines #L163 - L164 were not covered by tests
if tac:
echo("The selected CRN requires you to accept the following conditions and terms of use:\n")

Check warning on line 166 in src/aleph_client/models.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/models.py#L166

Added line #L166 was not covered by tests
if tac.filename:
echo(f"Filename: {tac.filename}")
echo(f"↳ {tac.url}\n")

Check warning on line 169 in src/aleph_client/models.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/models.py#L168-L169

Added lines #L168 - L169 were not covered by tests
if auto_accept:
echo("To proceed, enter “Yes I read and accept”: Yes I read and accept")
return True
return Prompt.ask("To proceed, enter “Yes I read and accept”").lower() == "yes i read and accept"
return None

Check warning on line 174 in src/aleph_client/models.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/models.py#L171-L174

Added lines #L171 - L174 were not covered by tests

def display_crn_specs(self):
echo(f"Hash: {self.hash}")
echo(f"Name: {self.name}")
Expand All @@ -163,3 +187,5 @@ def display_crn_specs(self):
echo(f"Support Qemu: {self.qemu_support}")
echo(f"Support Confidential: {self.confidential_computing}")
echo(f"Support GPU: {self.gpu_support}")
if self.terms_and_conditions:
echo(f"Terms & Conditions: {self.terms_and_conditions}")

Check warning on line 191 in src/aleph_client/models.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/models.py#L191

Added line #L191 was not covered by tests
Loading

0 comments on commit c6a1f28

Please sign in to comment.