Browse Source

Release 6.1.0

master v6.1.0
SporeStack 3 weeks ago
parent
commit
454a2a17c1
  1. 7
      CHANGELOG.md
  2. 2
      src/sporestack/__init__.py
  3. 59
      src/sporestack/api.py
  4. 26
      src/sporestack/api_client.py
  5. 85
      src/sporestack/cli.py
  6. 19
      src/sporestack/models.py
  7. 1
      tests/test_api_client.py

7
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

2
src/sporestack/__init__.py

@ -2,4 +2,4 @@
__all__ = ["api", "api_client", "exceptions"]
__version__ = "6.0.3"
__version__ = "6.1.0"

59
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]

26
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,

85
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:
"""

19
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

1
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...",

Loading…
Cancel
Save