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 group: test
image: python:3.7-alpine image: python:3.7-alpine
commands: commands:
- pip install pipenv==2022.4.20 - pip install pipenv==2022.9.4
- pipenv install --dev --deploy - pipenv install --dev --deploy
- pipenv run almake test-pytest # We only test with pytest on 3.7 - pipenv run almake test-pytest # We only test with pytest on 3.7
@ -22,7 +22,7 @@ pipeline:
group: test group: test
image: python:3.9-alpine image: python:3.9-alpine
commands: 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 - pre-commit run --all-files
- pipenv install --dev --deploy - pipenv install --dev --deploy
- pipenv run almake test - pipenv run almake test
@ -33,7 +33,7 @@ pipeline:
group: test group: test
image: python:3.10-alpine image: python:3.10-alpine
commands: 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 - pre-commit run --all-files
- pipenv install --dev --deploy - pipenv install --dev --deploy
- pipenv run almake test - 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] ## [Unreleased]
## [6.2.0 - 2022-09-07]
### Added
- Allow for new *beta* `--autorenew` feature with `sporestack server launch`.
## [6.1.0 - 2022-06-14] ## [6.1.0 - 2022-06-14]
### Changed ### Changed

View File

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

View File

@ -43,48 +43,43 @@ class ServerLaunch:
method = "POST" method = "POST"
class Request(BaseModel): class Request(BaseModel):
machine_id: str
days: int days: int
flavor: str flavor: str
ssh_key: str ssh_key: str
operating_system: str operating_system: str
currency: Optional[str] = None
"""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.""" """null is automatic, otherwise a string region slug."""
organization: Optional[str] = None token: str
"""Deprecated and ignored, don't use this."""
token: Optional[str] = None
"""Token to draw from when launching the server.""" """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""" """Don't launch, get a quote on how much it would cost"""
affiliate_token: Optional[str] = None affiliate_token: Optional[str] = None
affiliate_amount: None = None
"""Deprecated field"""
settlement_token: Optional[str] = None
"""Deprecated field. Use token instead."""
hostname: str = "" hostname: str = ""
"""Hostname to refer to your server by.""" """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): class Response(BaseModel):
payment: Payment payment: Payment
"""Deprecated, not needed when paying with token.""" """Deprecated, not needed when paying with token. Only used for quote."""
expiration: int expiration: int
machine_id: str machine_id: str
flavor: str
"""Deprecated, use ServerInfo instead."""
network_interfaces: List[NetworkInterface] = [] network_interfaces: List[NetworkInterface] = []
"""Deprecated, use ipv4/ipv6 from ServerInfo instead.""" """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.""" """Deprecated, use ServerInfo instead."""
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.""" """Deprecated, not needed when paying with token."""
warning: Optional[str] = None warning: Optional[str] = None
txid: Optional[str] = None flavor: str = ""
"""Deprecated.""" """Deprecated, use ServerInfo instead."""
class ServerTopup: class ServerTopup:
@ -92,17 +87,10 @@ class ServerTopup:
method = "POST" method = "POST"
class Request(BaseModel): class Request(BaseModel):
machine_id: str
days: int days: int
token: Optional[str] = None token: str
quote: bool = False 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_token: Optional[str] = None
affiliate_amount: None = None
"""Deprecated field"""
settlement_token: Optional[str] = None
"""Deprecated field. Use token instead."""
class Response(BaseModel): class Response(BaseModel):
machine_id: str machine_id: str
@ -112,9 +100,6 @@ class ServerTopup:
paid: bool = False paid: bool = False
"""Deprecated, not needed when paying with token.""" """Deprecated, not needed when paying with token."""
warning: Optional[str] = None warning: Optional[str] = None
txid: Optional[str] = None
"""Deprecated."""
latest_api_version: int = LATEST_API_VERSION
class ServerInfo: class ServerInfo:
@ -135,6 +120,7 @@ class ServerInfo:
"""Deprecated, use ipv4/ipv6 instead.""" """Deprecated, use ipv4/ipv6 instead."""
operating_system: str operating_system: str
hostname: str hostname: str
autorenew: bool
class ServerStart: class ServerStart:
@ -147,11 +133,22 @@ class ServerStop:
method = "POST" method = "POST"
# Deprecated in favor of ServerDestroy
class ServerDelete: class ServerDelete:
url = "/server/{machine_id}/delete" url = "/server/{machine_id}/delete"
method = "POST" method = "POST"
class ServerDestroy:
url = "/server/{machine_id}/destroy"
method = "POST"
class ServerForget:
url = "/server/{machine_id}/forget"
method = "POST"
class ServerRebuild: class ServerRebuild:
url = "/server/{machine_id}/rebuild" url = "/server/{machine_id}/rebuild"
method = "POST" method = "POST"

View File

@ -136,16 +136,16 @@ def launch(
flavor: str, flavor: str,
operating_system: str, operating_system: str,
ssh_key: str, ssh_key: str,
token: str,
api_endpoint: str = API_ENDPOINT, api_endpoint: str = API_ENDPOINT,
region: Optional[str] = None, region: Optional[str] = None,
token: Optional[str] = None,
retry: bool = False, retry: bool = False,
affiliate_token: Optional[str] = None, affiliate_token: Optional[str] = None,
quote: bool = False, quote: bool = False,
hostname: str = "", hostname: str = "",
autorenew: bool = False,
) -> api.ServerLaunch.Response: ) -> api.ServerLaunch.Response:
request = api.ServerLaunch.Request( request = api.ServerLaunch.Request(
machine_id=machine_id,
days=days, days=days,
token=token, token=token,
affiliate_token=affiliate_token, affiliate_token=affiliate_token,
@ -155,6 +155,7 @@ def launch(
ssh_key=ssh_key, ssh_key=ssh_key,
quote=quote, quote=quote,
hostname=hostname, hostname=hostname,
autorenew=autorenew,
) )
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)
@ -166,9 +167,8 @@ def launch(
def topup( def topup(
machine_id: str, machine_id: str,
days: int, days: int,
currency: str, token: str,
api_endpoint: str = API_ENDPOINT, api_endpoint: str = API_ENDPOINT,
token: Optional[str] = None,
retry: bool = False, retry: bool = False,
affiliate_token: Optional[str] = None, affiliate_token: Optional[str] = None,
quote: bool = False, quote: bool = False,
@ -177,9 +177,7 @@ def topup(
Topup a server. Topup a server.
""" """
request = api.ServerTopup.Request( request = api.ServerTopup.Request(
machine_id=machine_id,
days=days, days=days,
currency=currency,
token=token, token=token,
affiliate_token=affiliate_token, affiliate_token=affiliate_token,
quote=quote, quote=quote,
@ -207,11 +205,26 @@ def stop(machine_id: str, api_endpoint: str = API_ENDPOINT) -> None:
_api_request(url, empty_post=True) _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: 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) _api_request(url, empty_post=True)

View File

@ -113,6 +113,7 @@ def launch(
token: str = DEFAULT_TOKEN, token: str = DEFAULT_TOKEN,
region: Optional[str] = None, region: Optional[str] = None,
quote: bool = typer.Option(True, help="Require manual price confirmation."), quote: bool = typer.Option(True, help="Require manual price confirmation."),
autorenew: bool = typer.Option(False, help="BETA: Automatically renew server."),
) -> None: ) -> None:
""" """
Launch a server on SporeStack. Launch a server on SporeStack.
@ -151,12 +152,24 @@ def launch(
retry=True, retry=True,
quote=True, quote=True,
hostname=hostname, hostname=hostname,
autorenew=autorenew,
) )
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?"
typer.echo(msg, err=True) typer.echo(msg, err=True)
input("[Press ctrl+c to cancel, or enter to accept.]") 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 tries = 360
while tries > 0: while tries > 0:
response = api_client.launch( response = api_client.launch(
@ -168,6 +181,7 @@ def launch(
region=region, region=region,
token=_token, token=_token,
hostname=hostname, hostname=hostname,
autorenew=autorenew,
api_endpoint=get_api_endpoint(), api_endpoint=get_api_endpoint(),
retry=True, retry=True,
) )
@ -183,8 +197,6 @@ def launch(
raise typer.Exit(code=1) raise typer.Exit(code=1)
created_dict = response.dict() 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(pretty_machine_info(created_dict), err=True)
typer.echo(json.dumps(created_dict, indent=4)) typer.echo(json.dumps(created_dict, indent=4))
@ -213,7 +225,6 @@ def topup(
response = api_client.topup( response = api_client.topup(
machine_id=machine_id, machine_id=machine_id,
days=days, days=days,
currency="settlement",
api_endpoint=get_api_endpoint(), api_endpoint=get_api_endpoint(),
token=_token, token=_token,
retry=True, retry=True,
@ -226,16 +237,13 @@ def topup(
response = api_client.topup( response = api_client.topup(
machine_id=machine_id, machine_id=machine_id,
days=days, days=days,
currency="settlement",
api_endpoint=get_api_endpoint(), api_endpoint=get_api_endpoint(),
token=_token, token=_token,
retry=True, retry=True,
) )
assert response.payment.paid is True assert response.payment.paid is True
machine_info["expiration"] = response.expiration typer.echo(response.expiration)
save_machine_info(machine_info, overwrite=True)
typer.echo(machine_info["expiration"])
def server_info_path() -> Path: def server_info_path() -> Path:
@ -268,18 +276,6 @@ def token_path() -> Path:
return token_dir 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]: def get_machine_info(hostname: str) -> Dict[str, Any]:
""" """
Get info from disk. Get info from disk.
@ -435,7 +431,7 @@ def start(hostname: str) -> None:
@server_cli.command() @server_cli.command()
def stop(hostname: str) -> None: def stop(hostname: str) -> None:
""" """
Immediately kills the VM. Immediately shuts down the VM.
""" """
machine_info = get_machine_info(hostname) machine_info = get_machine_info(hostname)
machine_id = machine_info["machine_id"] machine_id = machine_info["machine_id"]
@ -443,17 +439,34 @@ def stop(hostname: str) -> None:
typer.echo(f"{hostname} stopped.") 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() @server_cli.command()
def delete(hostname: str) -> None: def delete(hostname: str) -> None:
""" """
Deletes the VM before expiration (no refunds/credits) 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_info = get_machine_info(hostname)
machine_id = machine_info["machine_id"] 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 # Also remove the .json file
server_info_path().joinpath(f"{hostname}.json").unlink() server_info_path().joinpath(f"{hostname}.json").unlink(missing_ok=True)
typer.echo(f"{hostname} was deleted.") typer.echo(f"{hostname} was destroyed.")
@server_cli.command() @server_cli.command()

View File

@ -29,13 +29,14 @@ def test_launch(mock_api_request: MagicMock) -> None:
operating_system="freebsd-12", operating_system="freebsd-12",
ssh_key="id-rsa...", ssh_key="id-rsa...",
flavor="aflavor", flavor="aflavor",
token="f" * 64,
) )
@patch("sporestack.api_client._api_request") @patch("sporestack.api_client._api_request")
def test_topup(mock_api_request: MagicMock) -> None: def test_topup(mock_api_request: MagicMock) -> None:
with pytest.raises(ValidationError): 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") @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: def test_delete(mock_api_request: MagicMock) -> None:
api_client.delete("dummymachineid") api_client.delete("dummymachineid")
mock_api_request.assert_called_once_with( 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
) )