From 454a2a17c1380dfa2ad05e83de8d9e4f1b672f77 Mon Sep 17 00:00:00 2001 From: SporeStack Date: Tue, 14 Jun 2022 02:40:11 +0000 Subject: [PATCH] Release 6.1.0 --- CHANGELOG.md | 7 +++ src/sporestack/__init__.py | 2 +- src/sporestack/api.py | 59 +++++++++++++++++-------- src/sporestack/api_client.py | 26 ++++++++++- src/sporestack/cli.py | 85 +++++++++++++++++++++++++++++++----- src/sporestack/models.py | 19 ++++++++ tests/test_api_client.py | 1 - 7 files changed, 166 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e38f4b9..9ce55a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [6.1.0 - 2022-06-14] + +### Changed + +- Use servers launched by token endpoint in `sporestack server list`. +- Send server hostname to SporeStack API at launch time. + ## [6.0.3 - 2022-04-22] ### Changed diff --git a/src/sporestack/__init__.py b/src/sporestack/__init__.py index 49bea37..6e58290 100644 --- a/src/sporestack/__init__.py +++ b/src/sporestack/__init__.py @@ -2,4 +2,4 @@ __all__ = ["api", "api_client", "exceptions"] -__version__ = "6.0.3" +__version__ = "6.1.0" diff --git a/src/sporestack/api.py b/src/sporestack/api.py index 64f35ea..1f462f1 100644 --- a/src/sporestack/api.py +++ b/src/sporestack/api.py @@ -9,27 +9,11 @@ from typing import List, Optional from pydantic import BaseModel -from .models import NetworkInterface, Payment +from .models import Flavor, NetworkInterface, Payment LATEST_API_VERSION = 3 -class TokenEnable: - """Deprecated: Use TokenAdd instead.""" - - url = "/token/{token}/enable" - method = "POST" - - class Request(BaseModel): - currency: str - dollars: int - affiliate_token: Optional[str] = None - - class Response(BaseModel): - token: str - payment: Payment - - class TokenAdd: url = "/token/{token}/add" method = "POST" @@ -67,29 +51,40 @@ class ServerLaunch: currency: Optional[str] = None """Currency only needs to be set if not paying with a token.""" region: Optional[str] = None + """null is automatic, otherwise a string region slug.""" organization: Optional[str] = None + """Deprecated and ignored, don't use this.""" token: Optional[str] = None + """Token to draw from when launching the server.""" quote: bool = False + """Don't launch, get a quote on how muchi t would cost""" affiliate_token: Optional[str] = None affiliate_amount: None = None """Deprecated field""" settlement_token: Optional[str] = None """Deprecated field. Use token instead.""" + hostname: str = "" + """Hostname to refer to your server by.""" class Response(BaseModel): payment: Payment + """Deprecated, not needed when paying with token.""" expiration: int machine_id: str - operating_system: str flavor: str + """Deprecated, use ServerInfo instead.""" network_interfaces: List[NetworkInterface] = [] + """Deprecated, use ipv4/ipv6 from ServerInfo instead.""" created_at: int = 0 region: Optional[str] = None + """Deprecated, use ServerInfo instead.""" latest_api_version: int = LATEST_API_VERSION created: bool = False paid: bool = False + """Deprecated, not needed when paying with token.""" warning: Optional[str] = None txid: Optional[str] = None + """Deprecated.""" class ServerTopup: @@ -112,10 +107,13 @@ class ServerTopup: class Response(BaseModel): machine_id: str payment: Payment + """Deprecated, not needed when paying with token.""" expiration: int paid: bool = False + """Deprecated, not needed when paying with token.""" warning: Optional[str] = None txid: Optional[str] = None + """Deprecated.""" latest_api_version: int = LATEST_API_VERSION @@ -128,8 +126,15 @@ class ServerInfo: expiration: int running: bool machine_id: str - network_interfaces: List[NetworkInterface] + ipv4: str + ipv6: str region: str + flavor: Flavor + deleted: bool + network_interfaces: List[NetworkInterface] + """Deprecated, use ipv4/ipv6 instead.""" + operating_system: str + hostname: str class ServerStart: @@ -150,3 +155,19 @@ class ServerDelete: class ServerRebuild: url = "/server/{machine_id}/rebuild" method = "POST" + + +class ServersLaunchedFromToken: + url = "/token/{token}/servers" + method = "GET" + + class Response(BaseModel): + servers: List[ServerInfo.Response] + + +class Flavors: + url = "/flavors" + method = "GET" + + class Response(BaseModel): + flavors: dict[str, Flavor] diff --git a/src/sporestack/api_client.py b/src/sporestack/api_client.py index 6f33f2b..3eaa00c 100644 --- a/src/sporestack/api_client.py +++ b/src/sporestack/api_client.py @@ -133,7 +133,6 @@ def _api_request( def launch( machine_id: str, days: int, - currency: str, flavor: str, operating_system: str, ssh_key: str, @@ -143,11 +142,11 @@ def launch( retry: bool = False, affiliate_token: Optional[str] = None, quote: bool = False, + hostname: str = "", ) -> api.ServerLaunch.Response: request = api.ServerLaunch.Request( machine_id=machine_id, days=days, - currency=currency, token=token, affiliate_token=affiliate_token, flavor=flavor, @@ -155,6 +154,7 @@ def launch( operating_system=operating_system, ssh_key=ssh_key, quote=quote, + hostname=hostname, ) url = api_endpoint + api.ServerLaunch.url.format(machine_id=machine_id) response = _api_request(url=url, json_params=request.dict(), retry=retry) @@ -236,6 +236,28 @@ def info(machine_id: str, api_endpoint: str = API_ENDPOINT) -> api.ServerInfo.Re return response_object +def servers_launched_from_token( + token: str, api_endpoint: str = API_ENDPOINT +) -> api.ServersLaunchedFromToken.Response: + """ + Returns info of servers launched from a given token. + """ + url = api_endpoint + api.ServersLaunchedFromToken.url.format(token=token) + response = _api_request(url) + response_object = api.ServersLaunchedFromToken.Response.parse_obj(response) + return response_object + + +def flavors(api_endpoint: str = API_ENDPOINT) -> api.Flavors.Response: + """ + Returns available flavors. + """ + url = api_endpoint + api.Flavors.url + response = _api_request(url) + response_object = api.Flavors.Response.parse_obj(response) + return response_object + + def token_add( token: str, dollars: int, diff --git a/src/sporestack/cli.py b/src/sporestack/cli.py index 73feb12..48d7c8f 100644 --- a/src/sporestack/cli.py +++ b/src/sporestack/cli.py @@ -145,12 +145,12 @@ def launch( flavor=flavor, operating_system=operating_system, ssh_key=ssh_key, - currency="settlement", region=region, token=_token, api_endpoint=get_api_endpoint(), retry=True, quote=True, + hostname=hostname, ) msg = f"Is {response.payment.usd} for {days} day(s) of {flavor} okay?" @@ -165,9 +165,9 @@ def launch( flavor=flavor, operating_system=operating_system, ssh_key=ssh_key, - currency="settlement", region=region, token=_token, + hostname=hostname, api_endpoint=get_api_endpoint(), retry=True, ) @@ -314,17 +314,63 @@ def pretty_machine_info(info: Dict[str, Any]) -> str: @server_cli.command(name="list") -def server_list() -> None: +def server_list(token: str = DEFAULT_TOKEN) -> None: """ - List all locally known servers. + List all locally known servers and all servers under the given token. """ from .exceptions import SporeStackUserError directory = server_info_path() - infos = [] + + _token = load_token(token) + + server_infos = api_client.servers_launched_from_token( + token=_token, api_endpoint=get_api_endpoint() + ).servers + machine_id_hostnames = {} + for hostname_json in os.listdir(directory): hostname = hostname_json.split(".")[0] saved_vm_info = get_machine_info(hostname) + machine_id_hostnames[saved_vm_info["machine_id"]] = hostname + + printed_machine_ids = [] + + for info in server_infos: + typer.echo() + + hostname = info.hostname + if hostname == "": + if info.machine_id in machine_id_hostnames: + hostname = machine_id_hostnames[info.machine_id] + if hostname != "": + typer.echo(f"Hostname: {hostname}") + + 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}") + 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.") + if info.deleted: + typer.echo("Server was deleted!") + + printed_machine_ids.append(info.machine_id) + + for hostname_json in os.listdir(directory): + hostname = hostname_json.split(".")[0] + saved_vm_info = get_machine_info(hostname) + machine_id = saved_vm_info["machine_id"] + if machine_id in printed_machine_ids: + continue + try: upstream_vm_info = api_client.info( machine_id=saved_vm_info["machine_id"], @@ -332,7 +378,8 @@ def server_list() -> None: ) saved_vm_info["expiration"] = upstream_vm_info.expiration saved_vm_info["running"] = upstream_vm_info.running - infos.append(saved_vm_info) + typer.echo() + typer.echo(pretty_machine_info(saved_vm_info)) except SporeStackUserError as e: expiration = saved_vm_info["expiration"] human_expiration = time.strftime( @@ -343,10 +390,6 @@ def server_list() -> None: msg += str(e) typer.echo(msg) - for info in infos: - typer.echo() - typer.echo(pretty_machine_info(info)) - typer.echo() @@ -426,6 +469,14 @@ def rebuild(hostname: str) -> None: typer.echo(f"{hostname} rebuilding.") +@server_cli.command() +def flavors() -> None: + """ + Returns available flavors. + """ + typer.echo(api_client.flavors(api_endpoint=get_api_endpoint())) + + def load_token(token: str) -> str: token_file = token_path().joinpath(f"{token}.json") if not token_file.exists(): @@ -581,6 +632,20 @@ def balance(token: str = typer.Argument(DEFAULT_TOKEN)) -> None: ) +@token_cli.command() +def servers(token: str = typer.Argument(DEFAULT_TOKEN)) -> None: + """ + Returns server info for servers launched by a given token. + """ + _token = load_token(token) + + typer.echo( + api_client.servers_launched_from_token( + token=_token, api_endpoint=get_api_endpoint() + ) + ) + + @token_cli.command(name="list") def token_list() -> None: """ diff --git a/src/sporestack/models.py b/src/sporestack/models.py index b72d30d..5fe8392 100644 --- a/src/sporestack/models.py +++ b/src/sporestack/models.py @@ -20,3 +20,22 @@ class Payment(BaseModel): uri: Optional[str] usd: str paid: bool + + +class Flavor(BaseModel): + # Unique string to identify the flavor that's sort of human readable. + slug: str + # Number of vCPU cores the server is given. + cores: int + # Memory in Megabytes + memory: int + # Disk in Gigabytes + disk: int + # USD cents per day + price: int + # IPv4 connectivity: "/32" + ipv4: str + # IPv6 connectivity: "/128" + ipv6: str + # Gigabytes of bandwidth per day + bandwidth: int diff --git a/tests/test_api_client.py b/tests/test_api_client.py index c4feb47..2034384 100644 --- a/tests/test_api_client.py +++ b/tests/test_api_client.py @@ -25,7 +25,6 @@ def test_launch(mock_api_request: MagicMock) -> None: with pytest.raises(ValidationError): api_client.launch( "dummymachineid", - currency="xmr", days=1, operating_system="freebsd-12", ssh_key="id-rsa...",