v6.2.0: Add beta flag: --autorenew for sporestack server launch
This commit is contained in:
parent
454a2a17c1
commit
3f2cd2c20b
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
|
|
||||||
__all__ = ["api", "api_client", "exceptions"]
|
__all__ = ["api", "api_client", "exceptions"]
|
||||||
|
|
||||||
__version__ = "6.1.0"
|
__version__ = "6.2.0"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue