v6.2.0: Add beta flag: --autorenew for sporestack server launch

This commit is contained in:
SporeStack 2022-09-07 21:40:02 +00:00
parent 454a2a17c1
commit 3f2cd2c20b
7 changed files with 95 additions and 65 deletions

View File

@ -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

View File

@ -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

View File

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

View File

@ -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"

View File

@ -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)

View File

@ -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()

View File

@ -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
)