Release 6.1.0
This commit is contained in:
parent
3c5f69c549
commit
454a2a17c1
|
@ -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,4 +2,4 @@
|
|||
|
||||
__all__ = ["api", "api_client", "exceptions"]
|
||||
|
||||
__version__ = "6.0.3"
|
||||
__version__ = "6.1.0"
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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…
Reference in New Issue