parent
454a2a17c1
commit
3f2cd2c20b
|
@ -3,7 +3,7 @@ pipeline:
|
|||
group: test
|
||||
image: python:3.7-alpine
|
||||
commands:
|
||||
- pip install pipenv==2022.4.20
|
||||
- pip install pipenv==2022.9.4
|
||||
- pipenv install --dev --deploy
|
||||
- pipenv run almake test-pytest # We only test with pytest on 3.7
|
||||
|
||||
|
@ -22,7 +22,7 @@ pipeline:
|
|||
group: test
|
||||
image: python:3.9-alpine
|
||||
commands:
|
||||
- pip install pipenv==2022.4.20 pre-commit==2.17.0
|
||||
- pip install pipenv==2022.9.4 pre-commit==2.22.0
|
||||
- pre-commit run --all-files
|
||||
- pipenv install --dev --deploy
|
||||
- pipenv run almake test
|
||||
|
@ -33,7 +33,7 @@ pipeline:
|
|||
group: test
|
||||
image: python:3.10-alpine
|
||||
commands:
|
||||
- pip install pipenv==2022.4.20 pre-commit==2.17.0
|
||||
- pip install pipenv==2022.9.4 pre-commit==2.22.0
|
||||
- pre-commit run --all-files
|
||||
- pipenv install --dev --deploy
|
||||
- pipenv run almake test
|
||||
|
|
|
@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [6.2.0 - 2022-09-07]
|
||||
|
||||
### Added
|
||||
|
||||
- Allow for new *beta* `--autorenew` feature with `sporestack server launch`.
|
||||
|
||||
## [6.1.0 - 2022-06-14]
|
||||
|
||||
### Changed
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
__all__ = ["api", "api_client", "exceptions"]
|
||||
|
||||
__version__ = "6.1.0"
|
||||
__version__ = "6.2.0"
|
||||
|
|
|
@ -43,48 +43,43 @@ class ServerLaunch:
|
|||
method = "POST"
|
||||
|
||||
class Request(BaseModel):
|
||||
machine_id: str
|
||||
days: int
|
||||
flavor: str
|
||||
ssh_key: str
|
||||
operating_system: str
|
||||
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: str
|
||||
"""Token to draw from when launching the server."""
|
||||
quote: bool = False
|
||||
"""Don't launch, get a quote on how muchi t would cost"""
|
||||
"""Don't launch, get a quote on how much it 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."""
|
||||
autorenew: bool = False
|
||||
"""
|
||||
Automatically renew the server with the token used, keeping it at 1 week
|
||||
expiration.
|
||||
|
||||
BETA FEATURE!!!
|
||||
"""
|
||||
|
||||
class Response(BaseModel):
|
||||
payment: Payment
|
||||
"""Deprecated, not needed when paying with token."""
|
||||
"""Deprecated, not needed when paying with token. Only used for quote."""
|
||||
expiration: int
|
||||
machine_id: 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."""
|
||||
flavor: str = ""
|
||||
"""Deprecated, use ServerInfo instead."""
|
||||
|
||||
|
||||
class ServerTopup:
|
||||
|
@ -92,17 +87,10 @@ class ServerTopup:
|
|||
method = "POST"
|
||||
|
||||
class Request(BaseModel):
|
||||
machine_id: str
|
||||
days: int
|
||||
token: Optional[str] = None
|
||||
token: str
|
||||
quote: bool = False
|
||||
currency: Optional[str] = None
|
||||
"""Currency only needs to be set if not paying with a token."""
|
||||
affiliate_token: Optional[str] = None
|
||||
affiliate_amount: None = None
|
||||
"""Deprecated field"""
|
||||
settlement_token: Optional[str] = None
|
||||
"""Deprecated field. Use token instead."""
|
||||
|
||||
class Response(BaseModel):
|
||||
machine_id: str
|
||||
|
@ -112,9 +100,6 @@ class ServerTopup:
|
|||
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
|
||||
|
||||
|
||||
class ServerInfo:
|
||||
|
@ -135,6 +120,7 @@ class ServerInfo:
|
|||
"""Deprecated, use ipv4/ipv6 instead."""
|
||||
operating_system: str
|
||||
hostname: str
|
||||
autorenew: bool
|
||||
|
||||
|
||||
class ServerStart:
|
||||
|
@ -147,11 +133,22 @@ class ServerStop:
|
|||
method = "POST"
|
||||
|
||||
|
||||
# Deprecated in favor of ServerDestroy
|
||||
class ServerDelete:
|
||||
url = "/server/{machine_id}/delete"
|
||||
method = "POST"
|
||||
|
||||
|
||||
class ServerDestroy:
|
||||
url = "/server/{machine_id}/destroy"
|
||||
method = "POST"
|
||||
|
||||
|
||||
class ServerForget:
|
||||
url = "/server/{machine_id}/forget"
|
||||
method = "POST"
|
||||
|
||||
|
||||
class ServerRebuild:
|
||||
url = "/server/{machine_id}/rebuild"
|
||||
method = "POST"
|
||||
|
|
|
@ -136,16 +136,16 @@ def launch(
|
|||
flavor: str,
|
||||
operating_system: str,
|
||||
ssh_key: str,
|
||||
token: str,
|
||||
api_endpoint: str = API_ENDPOINT,
|
||||
region: Optional[str] = None,
|
||||
token: Optional[str] = None,
|
||||
retry: bool = False,
|
||||
affiliate_token: Optional[str] = None,
|
||||
quote: bool = False,
|
||||
hostname: str = "",
|
||||
autorenew: bool = False,
|
||||
) -> api.ServerLaunch.Response:
|
||||
request = api.ServerLaunch.Request(
|
||||
machine_id=machine_id,
|
||||
days=days,
|
||||
token=token,
|
||||
affiliate_token=affiliate_token,
|
||||
|
@ -155,6 +155,7 @@ def launch(
|
|||
ssh_key=ssh_key,
|
||||
quote=quote,
|
||||
hostname=hostname,
|
||||
autorenew=autorenew,
|
||||
)
|
||||
url = api_endpoint + api.ServerLaunch.url.format(machine_id=machine_id)
|
||||
response = _api_request(url=url, json_params=request.dict(), retry=retry)
|
||||
|
@ -166,9 +167,8 @@ def launch(
|
|||
def topup(
|
||||
machine_id: str,
|
||||
days: int,
|
||||
currency: str,
|
||||
token: str,
|
||||
api_endpoint: str = API_ENDPOINT,
|
||||
token: Optional[str] = None,
|
||||
retry: bool = False,
|
||||
affiliate_token: Optional[str] = None,
|
||||
quote: bool = False,
|
||||
|
@ -177,9 +177,7 @@ def topup(
|
|||
Topup a server.
|
||||
"""
|
||||
request = api.ServerTopup.Request(
|
||||
machine_id=machine_id,
|
||||
days=days,
|
||||
currency=currency,
|
||||
token=token,
|
||||
affiliate_token=affiliate_token,
|
||||
quote=quote,
|
||||
|
@ -207,11 +205,26 @@ def stop(machine_id: str, api_endpoint: str = API_ENDPOINT) -> None:
|
|||
_api_request(url, empty_post=True)
|
||||
|
||||
|
||||
def destroy(machine_id: str, api_endpoint: str = API_ENDPOINT) -> None:
|
||||
"""
|
||||
Destroys the server.
|
||||
"""
|
||||
url = api_endpoint + api.ServerDestroy.url.format(machine_id=machine_id)
|
||||
_api_request(url, empty_post=True)
|
||||
|
||||
|
||||
def delete(machine_id: str, api_endpoint: str = API_ENDPOINT) -> None:
|
||||
"""
|
||||
Deletes the server.
|
||||
Deletes the server. (Deprecated, use destroy instead)
|
||||
"""
|
||||
url = api_endpoint + api.ServerDelete.url.format(machine_id=machine_id)
|
||||
destroy(machine_id, api_endpoint)
|
||||
|
||||
|
||||
def forget(machine_id: str, api_endpoint: str = API_ENDPOINT) -> None:
|
||||
"""
|
||||
Forget about a destroyed/deleted server.
|
||||
"""
|
||||
url = api_endpoint + api.ServerForget.url.format(machine_id=machine_id)
|
||||
_api_request(url, empty_post=True)
|
||||
|
||||
|
||||
|
|
|
@ -113,6 +113,7 @@ def launch(
|
|||
token: str = DEFAULT_TOKEN,
|
||||
region: Optional[str] = None,
|
||||
quote: bool = typer.Option(True, help="Require manual price confirmation."),
|
||||
autorenew: bool = typer.Option(False, help="BETA: Automatically renew server."),
|
||||
) -> None:
|
||||
"""
|
||||
Launch a server on SporeStack.
|
||||
|
@ -151,12 +152,24 @@ def launch(
|
|||
retry=True,
|
||||
quote=True,
|
||||
hostname=hostname,
|
||||
autorenew=autorenew,
|
||||
)
|
||||
|
||||
msg = f"Is {response.payment.usd} for {days} day(s) of {flavor} okay?"
|
||||
typer.echo(msg, err=True)
|
||||
input("[Press ctrl+c to cancel, or enter to accept.]")
|
||||
|
||||
if autorenew:
|
||||
typer.echo("Autorenew is a BETA feature!!!", err=True)
|
||||
typer.echo(
|
||||
"Server will be automatically renewed (from this token) to one week of expiration.", # noqa: E501
|
||||
err=True,
|
||||
)
|
||||
typer.echo(
|
||||
"If using this feature, watch your token balance and server expiration closely!", # noqa: E501
|
||||
err=True,
|
||||
)
|
||||
|
||||
tries = 360
|
||||
while tries > 0:
|
||||
response = api_client.launch(
|
||||
|
@ -168,6 +181,7 @@ def launch(
|
|||
region=region,
|
||||
token=_token,
|
||||
hostname=hostname,
|
||||
autorenew=autorenew,
|
||||
api_endpoint=get_api_endpoint(),
|
||||
retry=True,
|
||||
)
|
||||
|
@ -183,8 +197,6 @@ def launch(
|
|||
raise typer.Exit(code=1)
|
||||
|
||||
created_dict = response.dict()
|
||||
created_dict["vm_hostname"] = hostname
|
||||
save_machine_info(created_dict)
|
||||
typer.echo(pretty_machine_info(created_dict), err=True)
|
||||
typer.echo(json.dumps(created_dict, indent=4))
|
||||
|
||||
|
@ -213,7 +225,6 @@ def topup(
|
|||
response = api_client.topup(
|
||||
machine_id=machine_id,
|
||||
days=days,
|
||||
currency="settlement",
|
||||
api_endpoint=get_api_endpoint(),
|
||||
token=_token,
|
||||
retry=True,
|
||||
|
@ -226,16 +237,13 @@ def topup(
|
|||
response = api_client.topup(
|
||||
machine_id=machine_id,
|
||||
days=days,
|
||||
currency="settlement",
|
||||
api_endpoint=get_api_endpoint(),
|
||||
token=_token,
|
||||
retry=True,
|
||||
)
|
||||
assert response.payment.paid is True
|
||||
|
||||
machine_info["expiration"] = response.expiration
|
||||
save_machine_info(machine_info, overwrite=True)
|
||||
typer.echo(machine_info["expiration"])
|
||||
typer.echo(response.expiration)
|
||||
|
||||
|
||||
def server_info_path() -> Path:
|
||||
|
@ -268,18 +276,6 @@ def token_path() -> Path:
|
|||
return token_dir
|
||||
|
||||
|
||||
def save_machine_info(machine_info: Dict[str, Any], overwrite: bool = False) -> None:
|
||||
"""
|
||||
Save info to disk.
|
||||
"""
|
||||
directory = server_info_path()
|
||||
hostname = machine_info["vm_hostname"]
|
||||
json_file = directory / f"{hostname}.json"
|
||||
if overwrite is False:
|
||||
assert json_file.exists() is False, f"{json_file} already exists."
|
||||
json_file.write_text(json.dumps(machine_info))
|
||||
|
||||
|
||||
def get_machine_info(hostname: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get info from disk.
|
||||
|
@ -435,7 +431,7 @@ def start(hostname: str) -> None:
|
|||
@server_cli.command()
|
||||
def stop(hostname: str) -> None:
|
||||
"""
|
||||
Immediately kills the VM.
|
||||
Immediately shuts down the VM.
|
||||
"""
|
||||
machine_info = get_machine_info(hostname)
|
||||
machine_id = machine_info["machine_id"]
|
||||
|
@ -443,17 +439,34 @@ def stop(hostname: str) -> None:
|
|||
typer.echo(f"{hostname} stopped.")
|
||||
|
||||
|
||||
@server_cli.command()
|
||||
def destroy(hostname: str) -> None:
|
||||
"""
|
||||
Deletes/destroys the VM before expiration (no refunds/credits)
|
||||
"""
|
||||
_destroy(hostname)
|
||||
|
||||
|
||||
@server_cli.command()
|
||||
def delete(hostname: str) -> None:
|
||||
"""
|
||||
Deletes the VM before expiration (no refunds/credits)
|
||||
|
||||
Deprecated: Use destroy instead.
|
||||
"""
|
||||
_destroy(hostname)
|
||||
|
||||
|
||||
def _destroy(hostname: str) -> None:
|
||||
"""
|
||||
Deletes the VM before expiration (no refunds/credits)
|
||||
"""
|
||||
machine_info = get_machine_info(hostname)
|
||||
machine_id = machine_info["machine_id"]
|
||||
api_client.delete(machine_id=machine_id, api_endpoint=get_api_endpoint())
|
||||
api_client.destroy(machine_id=machine_id, api_endpoint=get_api_endpoint())
|
||||
# Also remove the .json file
|
||||
server_info_path().joinpath(f"{hostname}.json").unlink()
|
||||
typer.echo(f"{hostname} was deleted.")
|
||||
server_info_path().joinpath(f"{hostname}.json").unlink(missing_ok=True)
|
||||
typer.echo(f"{hostname} was destroyed.")
|
||||
|
||||
|
||||
@server_cli.command()
|
||||
|
|
|
@ -29,13 +29,14 @@ def test_launch(mock_api_request: MagicMock) -> None:
|
|||
operating_system="freebsd-12",
|
||||
ssh_key="id-rsa...",
|
||||
flavor="aflavor",
|
||||
token="f" * 64,
|
||||
)
|
||||
|
||||
|
||||
@patch("sporestack.api_client._api_request")
|
||||
def test_topup(mock_api_request: MagicMock) -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
api_client.topup("dummymachineid", currency="xmr", days=1)
|
||||
api_client.topup("dummymachineid", token="f" * 64, days=1)
|
||||
|
||||
|
||||
@patch("sporestack.api_client._api_request")
|
||||
|
@ -75,7 +76,7 @@ def test_info(mock_api_request: MagicMock) -> None:
|
|||
def test_delete(mock_api_request: MagicMock) -> None:
|
||||
api_client.delete("dummymachineid")
|
||||
mock_api_request.assert_called_once_with(
|
||||
"https://api.sporestack.com/server/dummymachineid/delete", empty_post=True
|
||||
"https://api.sporestack.com/server/dummymachineid/destroy", empty_post=True
|
||||
)
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue