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]
|
## [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]
|
## [6.0.3 - 2022-04-22]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
|
|
||||||
__all__ = ["api", "api_client", "exceptions"]
|
__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 pydantic import BaseModel
|
||||||
|
|
||||||
from .models import NetworkInterface, Payment
|
from .models import Flavor, NetworkInterface, Payment
|
||||||
|
|
||||||
LATEST_API_VERSION = 3
|
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:
|
class TokenAdd:
|
||||||
url = "/token/{token}/add"
|
url = "/token/{token}/add"
|
||||||
method = "POST"
|
method = "POST"
|
||||||
|
@ -67,29 +51,40 @@ class ServerLaunch:
|
||||||
currency: Optional[str] = None
|
currency: Optional[str] = None
|
||||||
"""Currency only needs to be set if not paying with a token."""
|
"""Currency only needs to be set if not paying with a token."""
|
||||||
region: Optional[str] = None
|
region: Optional[str] = None
|
||||||
|
"""null is automatic, otherwise a string region slug."""
|
||||||
organization: Optional[str] = None
|
organization: Optional[str] = None
|
||||||
|
"""Deprecated and ignored, don't use this."""
|
||||||
token: Optional[str] = None
|
token: Optional[str] = None
|
||||||
|
"""Token to draw from when launching the server."""
|
||||||
quote: bool = False
|
quote: bool = False
|
||||||
|
"""Don't launch, get a quote on how muchi t would cost"""
|
||||||
affiliate_token: Optional[str] = None
|
affiliate_token: Optional[str] = None
|
||||||
affiliate_amount: None = None
|
affiliate_amount: None = None
|
||||||
"""Deprecated field"""
|
"""Deprecated field"""
|
||||||
settlement_token: Optional[str] = None
|
settlement_token: Optional[str] = None
|
||||||
"""Deprecated field. Use token instead."""
|
"""Deprecated field. Use token instead."""
|
||||||
|
hostname: str = ""
|
||||||
|
"""Hostname to refer to your server by."""
|
||||||
|
|
||||||
class Response(BaseModel):
|
class Response(BaseModel):
|
||||||
payment: Payment
|
payment: Payment
|
||||||
|
"""Deprecated, not needed when paying with token."""
|
||||||
expiration: int
|
expiration: int
|
||||||
machine_id: str
|
machine_id: str
|
||||||
operating_system: str
|
|
||||||
flavor: str
|
flavor: str
|
||||||
|
"""Deprecated, use ServerInfo instead."""
|
||||||
network_interfaces: List[NetworkInterface] = []
|
network_interfaces: List[NetworkInterface] = []
|
||||||
|
"""Deprecated, use ipv4/ipv6 from ServerInfo instead."""
|
||||||
created_at: int = 0
|
created_at: int = 0
|
||||||
region: Optional[str] = None
|
region: Optional[str] = None
|
||||||
|
"""Deprecated, use ServerInfo instead."""
|
||||||
latest_api_version: int = LATEST_API_VERSION
|
latest_api_version: int = LATEST_API_VERSION
|
||||||
created: bool = False
|
created: bool = False
|
||||||
paid: bool = False
|
paid: bool = False
|
||||||
|
"""Deprecated, not needed when paying with token."""
|
||||||
warning: Optional[str] = None
|
warning: Optional[str] = None
|
||||||
txid: Optional[str] = None
|
txid: Optional[str] = None
|
||||||
|
"""Deprecated."""
|
||||||
|
|
||||||
|
|
||||||
class ServerTopup:
|
class ServerTopup:
|
||||||
|
@ -112,10 +107,13 @@ class ServerTopup:
|
||||||
class Response(BaseModel):
|
class Response(BaseModel):
|
||||||
machine_id: str
|
machine_id: str
|
||||||
payment: Payment
|
payment: Payment
|
||||||
|
"""Deprecated, not needed when paying with token."""
|
||||||
expiration: int
|
expiration: int
|
||||||
paid: bool = False
|
paid: bool = False
|
||||||
|
"""Deprecated, not needed when paying with token."""
|
||||||
warning: Optional[str] = None
|
warning: Optional[str] = None
|
||||||
txid: Optional[str] = None
|
txid: Optional[str] = None
|
||||||
|
"""Deprecated."""
|
||||||
latest_api_version: int = LATEST_API_VERSION
|
latest_api_version: int = LATEST_API_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
@ -128,8 +126,15 @@ class ServerInfo:
|
||||||
expiration: int
|
expiration: int
|
||||||
running: bool
|
running: bool
|
||||||
machine_id: str
|
machine_id: str
|
||||||
network_interfaces: List[NetworkInterface]
|
ipv4: str
|
||||||
|
ipv6: str
|
||||||
region: str
|
region: str
|
||||||
|
flavor: Flavor
|
||||||
|
deleted: bool
|
||||||
|
network_interfaces: List[NetworkInterface]
|
||||||
|
"""Deprecated, use ipv4/ipv6 instead."""
|
||||||
|
operating_system: str
|
||||||
|
hostname: str
|
||||||
|
|
||||||
|
|
||||||
class ServerStart:
|
class ServerStart:
|
||||||
|
@ -150,3 +155,19 @@ class ServerDelete:
|
||||||
class ServerRebuild:
|
class ServerRebuild:
|
||||||
url = "/server/{machine_id}/rebuild"
|
url = "/server/{machine_id}/rebuild"
|
||||||
method = "POST"
|
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(
|
def launch(
|
||||||
machine_id: str,
|
machine_id: str,
|
||||||
days: int,
|
days: int,
|
||||||
currency: str,
|
|
||||||
flavor: str,
|
flavor: str,
|
||||||
operating_system: str,
|
operating_system: str,
|
||||||
ssh_key: str,
|
ssh_key: str,
|
||||||
|
@ -143,11 +142,11 @@ def launch(
|
||||||
retry: bool = False,
|
retry: bool = False,
|
||||||
affiliate_token: Optional[str] = None,
|
affiliate_token: Optional[str] = None,
|
||||||
quote: bool = False,
|
quote: bool = False,
|
||||||
|
hostname: str = "",
|
||||||
) -> api.ServerLaunch.Response:
|
) -> api.ServerLaunch.Response:
|
||||||
request = api.ServerLaunch.Request(
|
request = api.ServerLaunch.Request(
|
||||||
machine_id=machine_id,
|
machine_id=machine_id,
|
||||||
days=days,
|
days=days,
|
||||||
currency=currency,
|
|
||||||
token=token,
|
token=token,
|
||||||
affiliate_token=affiliate_token,
|
affiliate_token=affiliate_token,
|
||||||
flavor=flavor,
|
flavor=flavor,
|
||||||
|
@ -155,6 +154,7 @@ def launch(
|
||||||
operating_system=operating_system,
|
operating_system=operating_system,
|
||||||
ssh_key=ssh_key,
|
ssh_key=ssh_key,
|
||||||
quote=quote,
|
quote=quote,
|
||||||
|
hostname=hostname,
|
||||||
)
|
)
|
||||||
url = api_endpoint + api.ServerLaunch.url.format(machine_id=machine_id)
|
url = api_endpoint + api.ServerLaunch.url.format(machine_id=machine_id)
|
||||||
response = _api_request(url=url, json_params=request.dict(), retry=retry)
|
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
|
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(
|
def token_add(
|
||||||
token: str,
|
token: str,
|
||||||
dollars: int,
|
dollars: int,
|
||||||
|
|
|
@ -145,12 +145,12 @@ def launch(
|
||||||
flavor=flavor,
|
flavor=flavor,
|
||||||
operating_system=operating_system,
|
operating_system=operating_system,
|
||||||
ssh_key=ssh_key,
|
ssh_key=ssh_key,
|
||||||
currency="settlement",
|
|
||||||
region=region,
|
region=region,
|
||||||
token=_token,
|
token=_token,
|
||||||
api_endpoint=get_api_endpoint(),
|
api_endpoint=get_api_endpoint(),
|
||||||
retry=True,
|
retry=True,
|
||||||
quote=True,
|
quote=True,
|
||||||
|
hostname=hostname,
|
||||||
)
|
)
|
||||||
|
|
||||||
msg = f"Is {response.payment.usd} for {days} day(s) of {flavor} okay?"
|
msg = f"Is {response.payment.usd} for {days} day(s) of {flavor} okay?"
|
||||||
|
@ -165,9 +165,9 @@ def launch(
|
||||||
flavor=flavor,
|
flavor=flavor,
|
||||||
operating_system=operating_system,
|
operating_system=operating_system,
|
||||||
ssh_key=ssh_key,
|
ssh_key=ssh_key,
|
||||||
currency="settlement",
|
|
||||||
region=region,
|
region=region,
|
||||||
token=_token,
|
token=_token,
|
||||||
|
hostname=hostname,
|
||||||
api_endpoint=get_api_endpoint(),
|
api_endpoint=get_api_endpoint(),
|
||||||
retry=True,
|
retry=True,
|
||||||
)
|
)
|
||||||
|
@ -314,17 +314,63 @@ def pretty_machine_info(info: Dict[str, Any]) -> str:
|
||||||
|
|
||||||
|
|
||||||
@server_cli.command(name="list")
|
@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
|
from .exceptions import SporeStackUserError
|
||||||
|
|
||||||
directory = server_info_path()
|
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):
|
for hostname_json in os.listdir(directory):
|
||||||
hostname = hostname_json.split(".")[0]
|
hostname = hostname_json.split(".")[0]
|
||||||
saved_vm_info = get_machine_info(hostname)
|
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:
|
try:
|
||||||
upstream_vm_info = api_client.info(
|
upstream_vm_info = api_client.info(
|
||||||
machine_id=saved_vm_info["machine_id"],
|
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["expiration"] = upstream_vm_info.expiration
|
||||||
saved_vm_info["running"] = upstream_vm_info.running
|
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:
|
except SporeStackUserError as e:
|
||||||
expiration = saved_vm_info["expiration"]
|
expiration = saved_vm_info["expiration"]
|
||||||
human_expiration = time.strftime(
|
human_expiration = time.strftime(
|
||||||
|
@ -343,10 +390,6 @@ def server_list() -> None:
|
||||||
msg += str(e)
|
msg += str(e)
|
||||||
typer.echo(msg)
|
typer.echo(msg)
|
||||||
|
|
||||||
for info in infos:
|
|
||||||
typer.echo()
|
|
||||||
typer.echo(pretty_machine_info(info))
|
|
||||||
|
|
||||||
typer.echo()
|
typer.echo()
|
||||||
|
|
||||||
|
|
||||||
|
@ -426,6 +469,14 @@ def rebuild(hostname: str) -> None:
|
||||||
typer.echo(f"{hostname} rebuilding.")
|
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:
|
def load_token(token: str) -> str:
|
||||||
token_file = token_path().joinpath(f"{token}.json")
|
token_file = token_path().joinpath(f"{token}.json")
|
||||||
if not token_file.exists():
|
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")
|
@token_cli.command(name="list")
|
||||||
def token_list() -> None:
|
def token_list() -> None:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -20,3 +20,22 @@ class Payment(BaseModel):
|
||||||
uri: Optional[str]
|
uri: Optional[str]
|
||||||
usd: str
|
usd: str
|
||||||
paid: bool
|
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):
|
with pytest.raises(ValidationError):
|
||||||
api_client.launch(
|
api_client.launch(
|
||||||
"dummymachineid",
|
"dummymachineid",
|
||||||
currency="xmr",
|
|
||||||
days=1,
|
days=1,
|
||||||
operating_system="freebsd-12",
|
operating_system="freebsd-12",
|
||||||
ssh_key="id-rsa...",
|
ssh_key="id-rsa...",
|
||||||
|
|
Loading…
Reference in New Issue