diff --git a/src/sporestack/__init__.py b/src/sporestack/__init__.py index c7b84b0..6774856 100644 --- a/src/sporestack/__init__.py +++ b/src/sporestack/__init__.py @@ -2,4 +2,4 @@ __all__ = ["api", "api_client", "exceptions"] -__version__ = "7.3.0" +__version__ = "8.0.0" diff --git a/src/sporestack/cli.py b/src/sporestack/cli.py index f675991..037e6c0 100644 --- a/src/sporestack/cli.py +++ b/src/sporestack/cli.py @@ -2,42 +2,15 @@ SporeStack CLI: `sporestack` """ -import importlib.util import json import logging import os -import sys import time from pathlib import Path -from types import ModuleType -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import Any, Dict, Optional import typer -from . import __version__ - - -def lazy_import(name: str) -> ModuleType: - """ - Lazily import a module. Helps speed up CLI performance. - """ - spec = importlib.util.find_spec(name) - assert spec is not None - assert spec.loader is not None - loader = importlib.util.LazyLoader(spec.loader) - spec.loader = loader - module = importlib.util.module_from_spec(spec) - sys.modules[name] = module - loader.exec_module(module) - return module - - -# For mypy -if TYPE_CHECKING: - from . import api_client -else: - api_client = lazy_import("sporestack.api_client") - HELP = """ SporeStack Python CLI @@ -81,9 +54,11 @@ WAITING_PAYMENT_TO_PROCESS = "Waiting for payment to process..." def get_api_endpoint() -> str: - api_endpoint = os.getenv("SPORESTACK_ENDPOINT", api_client.CLEARNET_ENDPOINT) + from .api_client import CLEARNET_ENDPOINT, TOR_ENDPOINT + + api_endpoint = os.getenv("SPORESTACK_ENDPOINT", CLEARNET_ENDPOINT) if os.getenv("SPORESTACK_USE_TOR_ENDPOINT", None) is not None: - api_endpoint = api_client.TOR_ENDPOINT + api_endpoint = TOR_ENDPOINT return api_endpoint @@ -119,12 +94,14 @@ def launch( """ Launch a server on SporeStack. """ - - from . import utils - typer.echo(f"Launching server with token {token}...", err=True) _token = load_token(token) + from . import utils + from .api_client import APIClient + + api_client = APIClient(api_endpoint=get_api_endpoint()) + typer.echo(f"Loading SSH key from {ssh_key_file}...") if not ssh_key_file.exists(): msg = f"{ssh_key_file} does not exist. " @@ -137,7 +114,7 @@ def launch( machine_id = utils.random_machine_id() if quote: - response = api_client.launch( + response = api_client.server_launch( machine_id=machine_id, days=days, flavor=flavor, @@ -145,8 +122,6 @@ def launch( ssh_key=ssh_key, region=region, token=_token, - api_endpoint=get_api_endpoint(), - retry=True, quote=True, hostname=hostname, autorenew=autorenew, @@ -168,7 +143,7 @@ def launch( tries = 360 while tries > 0: - response = api_client.launch( + response = api_client.server_launch( machine_id=machine_id, days=days, flavor=flavor, @@ -178,8 +153,6 @@ def launch( token=_token, hostname=hostname, autorenew=autorenew, - api_endpoint=get_api_endpoint(), - retry=True, ) if response.created is True: break @@ -193,11 +166,7 @@ def launch( raise typer.Exit(code=1) typer.echo( - pretty_machine_info( - api_client.info( - machine_id=machine_id, api_endpoint=get_api_endpoint() - ).dict() - ) + pretty_machine_info(api_client.server_info(machine_id=machine_id).dict()) ) @@ -212,19 +181,21 @@ def topup( Extend an existing SporeStack server's lifetime. """ + from .api_client import APIClient + + api_client = APIClient(api_endpoint=get_api_endpoint()) + machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token) _token = load_token(token) - response = api_client.topup( + api_client.server_topup( machine_id=machine_id, days=days, - api_endpoint=get_api_endpoint(), token=_token, - retry=True, ) - typer.echo(response.expiration) + typer.echo(f"Server topped up for {days} day(s)") def server_info_path() -> Path: @@ -310,13 +281,14 @@ def server_list( """ List all locally known servers and all servers under the given token. """ + from .api_client import APIClient from .exceptions import SporeStackUserError + api_client = APIClient(api_endpoint=get_api_endpoint()) + _token = load_token(token) - server_infos = api_client.servers_launched_from_token( - token=_token, api_endpoint=get_api_endpoint() - ).servers + server_infos = api_client.servers_launched_from_token(token=_token).servers machine_id_hostnames = {} if local: @@ -341,20 +313,21 @@ def server_list( typer.echo(f"Machine ID (keep this secret!): {info.machine_id}") typer.echo(f"IPv6: {info.network_interfaces[0].ipv6}") typer.echo(f"IPv4: {info.network_interfaces[0].ipv4}") - typer.echo(f"Running: {info.running}") typer.echo(f"Region: {info.region}") typer.echo(f"Flavor: {info.flavor.slug}") - typer.echo(f"Token: {info.token}") - typer.echo(f"Autorenew: {info.autorenew}") human_expiration = time.strftime( "%Y-%m-%d %H:%M:%S %z", time.localtime(info.expiration) ) typer.echo(f"Expiration: {info.expiration} ({human_expiration})") - time_to_live = info.expiration - int(time.time()) - hours = time_to_live // 3600 - typer.echo(f"Server will be deleted in {hours} hours.") + typer.echo(f"Token: {info.token}") if info.deleted: typer.echo("Server was deleted!") + else: + typer.echo(f"Running: {info.running}") + time_to_live = info.expiration - int(time.time()) + hours = time_to_live // 3600 + typer.echo(f"Server will be deleted in {hours} hours.") + typer.echo(f"Autorenew: {info.autorenew}") printed_machine_ids.append(info.machine_id) @@ -367,9 +340,8 @@ def server_list( continue try: - upstream_vm_info = api_client.info( - machine_id=saved_vm_info["machine_id"], - api_endpoint=get_api_endpoint(), + upstream_vm_info = api_client.server_info( + machine_id=saved_vm_info["machine_id"] ) saved_vm_info["expiration"] = upstream_vm_info.expiration saved_vm_info["running"] = upstream_vm_info.running @@ -411,9 +383,11 @@ def _get_machine_id(machine_id: str, hostname: str, token: str) -> str: _token = load_token(token) - for server in api_client.servers_launched_from_token( - token=_token, api_endpoint=get_api_endpoint() - ).servers: + from .api_client import APIClient + + api_client = APIClient(api_endpoint=get_api_endpoint()) + + for server in api_client.servers_launched_from_token(token=_token).servers: if server.hostname == hostname: return server.machine_id @@ -429,18 +403,38 @@ def info(hostname: str = "", machine_id: str = "", token: str = DEFAULT_TOKEN) - Info on the VM """ machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token) + from .api_client import APIClient + + api_client = APIClient(api_endpoint=get_api_endpoint()) typer.echo( - api_client.info(machine_id=machine_id, api_endpoint=get_api_endpoint()).json() + pretty_machine_info(api_client.server_info(machine_id=machine_id).dict()) ) +@server_cli.command(name="json") +def server_info_json( + hostname: str = "", machine_id: str = "", token: str = DEFAULT_TOKEN +) -> None: + """ + Info on the VM, in JSON format + """ + machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token) + from .api_client import APIClient + + api_client = APIClient(api_endpoint=get_api_endpoint()) + typer.echo(api_client.server_info(machine_id=machine_id).json()) + + @server_cli.command() def start(hostname: str = "", machine_id: str = "", token: str = DEFAULT_TOKEN) -> None: """ Boots the VM. """ machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token) - api_client.start(machine_id=machine_id, api_endpoint=get_api_endpoint()) + from .api_client import APIClient + + api_client = APIClient(api_endpoint=get_api_endpoint()) + api_client.server_start(machine_id=machine_id) typer.echo(f"{hostname} started.") @@ -450,7 +444,10 @@ def stop(hostname: str = "", machine_id: str = "", token: str = DEFAULT_TOKEN) - Immediately shuts down the VM. """ machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token) - api_client.stop(machine_id=machine_id, api_endpoint=get_api_endpoint()) + from .api_client import APIClient + + api_client = APIClient(api_endpoint=get_api_endpoint()) + api_client.server_stop(machine_id=machine_id) typer.echo(f"{hostname} stopped.") @@ -462,7 +459,10 @@ def autorenew_enable( Enable autorenew on a server. """ machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token) - api_client.autorenew_enable(machine_id=machine_id, api_endpoint=get_api_endpoint()) + from .api_client import APIClient + + api_client = APIClient(api_endpoint=get_api_endpoint()) + api_client.autorenew_enable(machine_id=machine_id) typer.echo("Autorenew enabled.") @@ -474,19 +474,25 @@ def autorenew_disable( Disable autorenew on a server. """ machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token) - api_client.autorenew_disable(machine_id=machine_id, api_endpoint=get_api_endpoint()) + from .api_client import APIClient + + api_client = APIClient(api_endpoint=get_api_endpoint()) + api_client.autorenew_disable(machine_id=machine_id) typer.echo("Autorenew disabled.") @server_cli.command() -def destroy( +def delete( hostname: str = "", machine_id: str = "", token: str = DEFAULT_TOKEN ) -> None: """ - Deletes/destroys the VM before expiration (no refunds/credits) + Delete the VM before its expiration """ machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token) - api_client.destroy(machine_id=machine_id, api_endpoint=get_api_endpoint()) + from .api_client import APIClient + + api_client = APIClient(api_endpoint=get_api_endpoint()) + api_client.server_delete(machine_id=machine_id) # Also remove the .json file server_info_path().joinpath(f"{hostname}.json").unlink(missing_ok=True) typer.echo(f"{machine_id} was destroyed.") @@ -500,7 +506,10 @@ def forget( Forget about a deleted server so that it doesn't show up in server list. """ machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token) - api_client.forget(machine_id=machine_id, api_endpoint=get_api_endpoint()) + from .api_client import APIClient + + api_client = APIClient(api_endpoint=get_api_endpoint()) + api_client.server_forget(machine_id=machine_id) typer.echo(f"{machine_id} was forgotten.") @@ -514,7 +523,10 @@ def rebuild( Will take a couple minutes to complete after the request is made. """ machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token) - api_client.rebuild(machine_id=machine_id, api_endpoint=get_api_endpoint()) + from .api_client import APIClient + + api_client = APIClient(api_endpoint=get_api_endpoint()) + api_client.server_rebuild(machine_id=machine_id) typer.echo(f"{hostname} rebuilding.") @@ -523,7 +535,10 @@ def flavors() -> None: """ Returns available flavors. """ - flavors = api_client.flavors(api_endpoint=get_api_endpoint()).flavors + from .api_client import APIClient + + api_client = APIClient(api_endpoint=get_api_endpoint()) + flavors = api_client.flavors().flavors for flavor in flavors: typer.echo(f"{flavor}: {flavors[flavor]}") @@ -533,9 +548,10 @@ def operating_systems() -> None: """ Returns available operating systems. """ - os_list = api_client.operating_systems( - api_endpoint=get_api_endpoint() - ).operating_systems + from .api_client import APIClient + + api_client = APIClient(api_endpoint=get_api_endpoint()) + os_list = api_client.operating_systems().operating_systems for operating_system in os_list: typer.echo(operating_system) @@ -590,11 +606,14 @@ def token_create( typer.echo("Token already created! Did you mean to `topup`?", err=True) raise typer.Exit(1) + from .api_client import APIClient + + api_client = APIClient(api_endpoint=get_api_endpoint()) + response = api_client.token_add( token=_token, dollars=dollars, currency=currency, - api_endpoint=get_api_endpoint(), retry=True, ) @@ -615,7 +634,6 @@ def token_create( token=_token, dollars=dollars, currency=currency, - api_endpoint=get_api_endpoint(), retry=True, ) if response.payment.paid is True: @@ -649,11 +667,14 @@ def token_topup( """ token = load_token(token) + from .api_client import APIClient + + api_client = APIClient(api_endpoint=get_api_endpoint()) + response = api_client.token_add( token, dollars, currency=currency, - api_endpoint=get_api_endpoint(), retry=True, ) @@ -672,7 +693,6 @@ def token_topup( token, dollars, currency=currency, - api_endpoint=get_api_endpoint(), retry=True, ) # Waiting for payment to set in. @@ -690,9 +710,11 @@ def balance(token: str = typer.Argument(DEFAULT_TOKEN)) -> None: """ _token = load_token(token) - typer.echo( - api_client.token_balance(token=_token, api_endpoint=get_api_endpoint()).usd - ) + from .api_client import APIClient + + api_client = APIClient(api_endpoint=get_api_endpoint()) + + typer.echo(api_client.token_balance(token=_token).usd) @token_cli.command() @@ -702,11 +724,11 @@ def servers(token: str = typer.Argument(DEFAULT_TOKEN)) -> None: """ _token = load_token(token) - typer.echo( - api_client.servers_launched_from_token( - token=_token, api_endpoint=get_api_endpoint() - ) - ) + from .api_client import APIClient + + api_client = APIClient(api_endpoint=get_api_endpoint()) + + typer.echo(api_client.servers_launched_from_token(token=_token)) @token_cli.command(name="list") @@ -728,6 +750,8 @@ def version() -> None: """ Returns the installed version. """ + from . import __version__ + typer.echo(__version__) @@ -737,6 +761,8 @@ def api_endpoint() -> None: Prints the selected API endpoint: Env var: SPORESTACK_ENDPOINT, or, SPORESTACK_USE_TOR=1 """ + from . import api_client + endpoint = get_api_endpoint() if ".onion" in endpoint: typer.echo(f"{endpoint} using {api_client._get_tor_proxy()}")