Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create Instance #178

Merged
merged 7 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/aleph_client/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@
Aleph Client command-line interface.
"""

import typer

from aleph_client.utils import AsyncTyper

from .commands import about, account, aggregate, domain, files, message, node, program
from .commands import (
about,
account,
aggregate,
domain,
files,
instance,
message,
node,
program,
)

app = AsyncTyper()

Expand All @@ -29,6 +37,9 @@

app.add_typer(node.app, name="node", help="Get node info on aleph.im network")
app.add_typer(domain.app, name="domain", help="Manage custom Domain (dns) on aleph.im")
app.add_typer(
instance.app, name="instance", help="Manage instances (VMs) on aleph.im network"
)

if __name__ == "__main__":
app()
8 changes: 8 additions & 0 deletions src/aleph_client/commands/help_strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,11 @@
CUSTOM_DOMAIN_OWNER_ADDRESS = "Owner address, default current account"
CUSTOM_DOMAIN_NAME = "Domain name. ex: aleph.im"
CUSTOM_DOMAIN_ITEM_HASH = "Item hash"
PERSISTENT_VOLUME = """Persistent volumes are allocated on the host machine and are not deleted when the VM is stopped.\n
Requires at least a "mount" and "size_mib". For more info, see the docs: https://docs.aleph.im/computing/volumes/persistent/\n
Example: --persistent_volume persistence=host,size_mib=100,mount=/opt/data"""
EPHEMERAL_VOLUME = """Ephemeral volumes are allocated on the host machine when the VM is started and deleted when the VM is stopped.\n
Example: --ephemeral-volume size_mib=100,mount=/tmp/data"""
IMMUATABLE_VOLUME = """Immutable volumes are pinned on the network and can be used by multiple VMs at the same time. They are read-only and useful for setting up libraries or other dependencies.\n
Requires at least a "ref" (message hash) and "mount" path. "use_latest" is True by default, to use the latest version of the volume, if it has been amended. See the docs for more info: https://docs.aleph.im/computing/volumes/immutable/\n
Example: --immutable-volume ref=25a393222692c2f73489dc6710ae87605a96742ceef7b91de4d7ec34bb688d94,mount=/lib/python3.8/site-packages"""
211 changes: 211 additions & 0 deletions src/aleph_client/commands/instance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import logging
from base64 import b16decode, b32encode
from pathlib import Path
from typing import List, Optional, Union

import typer
from aleph.sdk import AlephHttpClient, AuthenticatedAlephHttpClient
from aleph.sdk.account import _load_account
from aleph.sdk.conf import settings as sdk_settings
from aleph.sdk.exceptions import ForgottenMessageError, MessageNotFoundError, InsufficientFundsError
from aleph.sdk.types import AccountFromPrivateKey, StorageEnum
from aleph_message.models import InstanceMessage, ItemHash, StoreMessage

from aleph_client.commands import help_strings
from aleph_client.commands.utils import (
default_prompt,
get_or_prompt_volumes,
setup_logging,
validated_int_prompt,
validated_prompt,
)
from aleph_client.conf import settings
from aleph_client.utils import AsyncTyper

logger = logging.getLogger(__name__)
app = AsyncTyper()


def load_ssh_pubkey(ssh_pubkey_file: Path) -> str:
with open(ssh_pubkey_file, "r") as f:
return f.read().strip()


@app.command()
async def create(
channel: Optional[str] = typer.Option(default=None, help=help_strings.CHANNEL),
memory: int = typer.Option(
sdk_settings.DEFAULT_VM_MEMORY, help="Maximum memory allocation on vm in MiB"
),
vcpus: int = typer.Option(
sdk_settings.DEFAULT_VM_VCPUS, help="Number of virtual cpus to allocate."
),
timeout_seconds: float = typer.Option(
sdk_settings.DEFAULT_VM_TIMEOUT,
help="If vm is not called after [timeout_seconds] it will shutdown",
),
private_key: Optional[str] = typer.Option(
sdk_settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY
),
private_key_file: Optional[Path] = typer.Option(
sdk_settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE
),
ssh_pubkey_file: Path = typer.Option(
Path("~/.ssh/id_rsa.pub").expanduser(),
help="Path to a public ssh key to be added to the instance.",
),
print_messages: bool = typer.Option(False),
rootfs: str = typer.Option(
settings.DEFAULT_ROOTFS_ID,
help="Hash of the rootfs to use for your instance. Defaults to aleph debian with Python3.8 and node. You can also create your own rootfs and pin it",
),
rootfs_name: str = typer.Option(
settings.DEFAULT_ROOTFS_NAME,
help="Name of the rootfs to use for your instance. If not set, content.metadata.name of the --rootfs store message will be used.",
),
rootfs_size: int = typer.Option(
settings.DEFAULT_ROOTFS_SIZE,
help="Size of the rootfs to use for your instance. If not set, content.size of the --rootfs store message will be used.",
),
debug: bool = False,
persistent_volume: Optional[List[str]] = typer.Option(
None, help=help_strings.PERSISTENT_VOLUME
),
ephemeral_volume: Optional[List[str]] = typer.Option(
None, help=help_strings.EPHEMERAL_VOLUME
),
immutable_volume: Optional[List[str]] = typer.Option(
None,
help=help_strings.IMMUATABLE_VOLUME,
),
):
"""Register a new instance on aleph.im"""

setup_logging(debug)

def validate_ssh_pubkey_file(file: Union[str, Path]) -> Path:
if isinstance(file, str):
file = Path(file).expanduser()
if not file.exists():
raise ValueError(f"{file} does not exist")
if not file.is_file():
raise ValueError(f"{file} is not a file")
return file

try:
validate_ssh_pubkey_file(ssh_pubkey_file)
except ValueError:
ssh_pubkey_file = Path(
validated_prompt(
f"{ssh_pubkey_file} does not exist. Please enter a path to a public ssh key to be added to the instance.",
validate_ssh_pubkey_file,
)
)

ssh_pubkey = load_ssh_pubkey(ssh_pubkey_file)

account: AccountFromPrivateKey = _load_account(private_key, private_key_file)

rootfs = default_prompt("Hash of the rootfs to use for your instance", rootfs)

async with AlephHttpClient(api_server=sdk_settings.API_HOST) as client:
rootfs_message: StoreMessage = await client.get_message(
item_hash=rootfs, message_type=StoreMessage
)
if not rootfs_message:
typer.echo("Given rootfs volume does not exist on aleph.im")
raise typer.Exit(code=1)
if rootfs_name is None and rootfs_message.content.metadata:
rootfs_name = rootfs_message.content.metadata.get("name", None)
if rootfs_size is None and rootfs_message.content.size:
rootfs_size = rootfs_message.content.size

rootfs_name = default_prompt(
f"Name of the rootfs to use for your instance", default=rootfs_name
)

rootfs_size = validated_int_prompt(
f"Size in MiB?", rootfs_size, min_value=2000, max_value=100000
)

volumes = get_or_prompt_volumes(
persistent_volume=persistent_volume,
ephemeral_volume=ephemeral_volume,
immutable_volume=immutable_volume,
)

async with AuthenticatedAlephHttpClient(
account=account, api_server=sdk_settings.API_HOST
) as client:
try:
message, status = await client.create_instance(
sync=True,
rootfs=rootfs,
rootfs_size=rootfs_size,
rootfs_name=rootfs_name,
storage_engine=StorageEnum.storage,
channel=channel,
memory=memory,
vcpus=vcpus,
timeout_seconds=timeout_seconds,
volumes=volumes,
ssh_keys=[ssh_pubkey],
)
except InsufficientFundsError as e:
typer.echo(
f"Instance creation failed due to insufficient funds.\n"
f"{account.get_address()} on {account.CHAIN} has {e.available_funds} ALEPH but needs {e.required_funds} ALEPH."
)
raise typer.Exit(code=1)
if print_messages:
typer.echo(f"{message.json(indent=4)}")

item_hash: ItemHash = message.item_hash
hash_base32 = (
b32encode(b16decode(item_hash.upper())).strip(b"=").lower().decode()
)

typer.echo(
f"\nYour instance has been deployed on aleph.im\n\n"
f"Your SSH key has been added to the instance. You can connect in a few minutes to it using:\n"
# TODO: Resolve to IPv6 address
f" ssh -i {ssh_pubkey_file} root@{hash_base32}.aleph.sh\n\n"
"Also available on:\n"
f" {settings.VM_URL_PATH.format(hash=item_hash)}\n"
"Visualise on:\n https://explorer.aleph.im/address/"
f"{message.chain}/{message.sender}/message/INSTANCE/{item_hash}\n"
)


@app.command()
def delete(
item_hash: str,
private_key: Optional[str] = sdk_settings.PRIVATE_KEY_STRING,
private_key_file: Optional[Path] = sdk_settings.PRIVATE_KEY_FILE,
debug: bool = False,
):
"""Delete an instance, unallocating all resources associated with it. Immutable volumes will not be deleted."""

setup_logging(debug)

account = _load_account(private_key, private_key_file)

with AuthenticatedAlephHttpClient(
account=account, api_server=sdk_settings.API_HOST
) as client:
try:
existing_message: InstanceMessage = client.get_message(
item_hash=item_hash, message_type=InstanceMessage
)
except MessageNotFoundError:
typer.echo("Instance does not exist")
raise typer.Exit(code=1)
except ForgottenMessageError:
typer.echo("Instance already forgotten")
raise typer.Exit(code=1)
if existing_message.sender != account.get_address():
typer.echo("You are not the owner of this instance")
raise typer.Exit(code=1)

message, status = client.forget(hashes=[item_hash])
typer.echo(f"{message.json(indent=4)}")
59 changes: 15 additions & 44 deletions src/aleph_client/commands/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@

from aleph_client.commands import help_strings
from aleph_client.commands.utils import (
get_or_prompt_volumes,
input_multiline,
prompt_for_volumes,
setup_logging,
volume_to_dict,
yes_no_input,
)
from aleph_client.conf import settings
Expand Down Expand Up @@ -62,34 +61,24 @@ def upload(
None,
help="Hash of the runtime to use for your program. Defaults to aleph debian with Python3.8 and node. You can also create your own runtime and pin it",
),
beta: bool = typer.Option(False),
beta: bool = typer.Option(
False,
help="If true, you will be prompted to add message subscriptions to your program",
),
debug: bool = False,
persistent: bool = False,
persistent_volume: Optional[List[str]] = typer.Option(
None,
help="""Takes 3 parameters
A persistent volume is allocated on the host machine at any time
eg: Use , to seperate the parameters and no spaces
--persistent_volume persistence=host,name=my-volume,size=100 ./my-program main:app
""",
None, help=help_strings.PERSISTENT_VOLUME
),
ephemeral_volume: Optional[List[str]] = typer.Option(
None,
help="""Takes 1 parameter Only
Ephemeral volumes can move and be removed by the host,Garbage collected basically, when the VM isn't running
eg: Use , to seperate the parameters and no spaces
--ephemeral-volume size_mib=100 ./my-program main:app """,
None, help=help_strings.EPHEMERAL_VOLUME
),
immutable_volume: Optional[List[str]] = typer.Option(
None,
help="""Takes 3 parameters
Immutable volume is one whose contents do not change
eg: Use , to seperate the parameters and no spaces
--immutable-volume ref=25a393222692c2f73489dc6710ae87605a96742ceef7b91de4d7ec34bb688d94,use_latest=true,mount=/mnt/volume ./my-program main:app
""",
help=help_strings.IMMUATABLE_VOLUME,
),
):
"""Register a program to run on aleph.im virtual machines from a zip archive."""
"""Register a program to run on aleph.im. For more information, see https://docs.aleph.im/computing/"""

setup_logging(debug)

Expand All @@ -112,29 +101,11 @@ def upload(
or sdk_settings.DEFAULT_RUNTIME_ID
)

volumes = []

# Check if the volumes are empty
if (
persistent_volume is None
or ephemeral_volume is None
or immutable_volume is None
):
for volume in prompt_for_volumes():
volumes.append(volume)
typer.echo("\n")

# else Parse all the volumes that have passed as the cli parameters and put it into volume list
else:
if len(persistent_volume) > 0:
persistent_volume_dict = volume_to_dict(volume=persistent_volume)
volumes.append(persistent_volume_dict)
if len(ephemeral_volume) > 0:
ephemeral_volume_dict = volume_to_dict(volume=ephemeral_volume)
volumes.append(ephemeral_volume_dict)
if len(immutable_volume) > 0:
immutable_volume_dict = volume_to_dict(volume=immutable_volume)
volumes.append(immutable_volume_dict)
volumes = get_or_prompt_volumes(
persistent_volume=persistent_volume,
ephemeral_volume=ephemeral_volume,
immutable_volume=immutable_volume,
)

subscriptions: Optional[List[Dict]]
if beta and yes_no_input("Subscribe to messages ?", default=False):
Expand Down Expand Up @@ -200,7 +171,7 @@ def upload(
)

typer.echo(
f"Your program has been uploaded on aleph.im .\n\n"
f"Your program has been uploaded on aleph.im\n\n"
"Available on:\n"
f" {settings.VM_URL_PATH.format(hash=item_hash)}\n"
f" {settings.VM_URL_HOST.format(hash_base32=hash_base32)}\n"
Expand Down
Loading
Loading