7.0.0
This commit is contained in:
parent
3f2cd2c20b
commit
6582917898
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -7,12 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [7.0.0 - 2022-09-07]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `sporestack server list` now accepts `--local` or `--no-local`.
|
||||||
|
- `sporestack server operating-systems`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `sporestack server` subcommands take `--hostname` or `--machine-id`.
|
||||||
|
- `sporestack server flavors` output is slightly more readable.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- `sporestack server delete` (in favor of: `sporestack server destroy`)
|
||||||
|
- `sporestack server get-attribute`
|
||||||
|
|
||||||
## [6.2.0 - 2022-09-07]
|
## [6.2.0 - 2022-09-07]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Allow for new *beta* `--autorenew` feature with `sporestack server launch`.
|
- Allow for new *beta* `--autorenew` feature with `sporestack server launch`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- No longer save server JSON to disk for new servers.
|
||||||
|
|
||||||
## [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.2.0"
|
__version__ = "7.0.0"
|
||||||
|
|
|
@ -168,3 +168,11 @@ class Flavors:
|
||||||
|
|
||||||
class Response(BaseModel):
|
class Response(BaseModel):
|
||||||
flavors: dict[str, Flavor]
|
flavors: dict[str, Flavor]
|
||||||
|
|
||||||
|
|
||||||
|
class OperatingSystems:
|
||||||
|
url = "/operatingsystems"
|
||||||
|
method = "GET"
|
||||||
|
|
||||||
|
class Response(BaseModel):
|
||||||
|
operating_systems: list[str]
|
||||||
|
|
|
@ -271,6 +271,18 @@ def flavors(api_endpoint: str = API_ENDPOINT) -> api.Flavors.Response:
|
||||||
return response_object
|
return response_object
|
||||||
|
|
||||||
|
|
||||||
|
def operating_systems(
|
||||||
|
api_endpoint: str = API_ENDPOINT,
|
||||||
|
) -> api.OperatingSystems.Response:
|
||||||
|
"""
|
||||||
|
Returns available operating systems.
|
||||||
|
"""
|
||||||
|
url = api_endpoint + api.OperatingSystems.url
|
||||||
|
response = _api_request(url)
|
||||||
|
response_object = api.OperatingSystems.Response.parse_obj(response)
|
||||||
|
return response_object
|
||||||
|
|
||||||
|
|
||||||
def token_add(
|
def token_add(
|
||||||
token: str,
|
token: str,
|
||||||
dollars: int,
|
dollars: int,
|
||||||
|
|
|
@ -60,10 +60,11 @@ os.umask(0o0077)
|
||||||
|
|
||||||
cli = typer.Typer(help=HELP)
|
cli = typer.Typer(help=HELP)
|
||||||
|
|
||||||
token_cli = typer.Typer()
|
|
||||||
HOME = Path(_home)
|
HOME = Path(_home)
|
||||||
|
|
||||||
|
token_cli = typer.Typer(help="Commands to interact with SporeStack tokens")
|
||||||
cli.add_typer(token_cli, name="token")
|
cli.add_typer(token_cli, name="token")
|
||||||
server_cli = typer.Typer()
|
server_cli = typer.Typer(help="Commands to interact with SporeStack servers")
|
||||||
cli.add_typer(server_cli, name="server")
|
cli.add_typer(server_cli, name="server")
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
@ -105,7 +106,7 @@ Press ctrl+c to abort."""
|
||||||
|
|
||||||
@server_cli.command()
|
@server_cli.command()
|
||||||
def launch(
|
def launch(
|
||||||
hostname: str,
|
hostname: str = "",
|
||||||
days: int = typer.Option(...),
|
days: int = typer.Option(...),
|
||||||
operating_system: str = typer.Option(...),
|
operating_system: str = typer.Option(...),
|
||||||
ssh_key_file: Path = DEFAULT_SSH_KEY_FILE,
|
ssh_key_file: Path = DEFAULT_SSH_KEY_FILE,
|
||||||
|
@ -124,10 +125,6 @@ def launch(
|
||||||
typer.echo(f"Launching server with token {token}...", err=True)
|
typer.echo(f"Launching server with token {token}...", err=True)
|
||||||
_token = load_token(token)
|
_token = load_token(token)
|
||||||
|
|
||||||
if machine_exists(hostname):
|
|
||||||
typer.echo(f"{hostname} already created.")
|
|
||||||
raise typer.Exit(code=1)
|
|
||||||
|
|
||||||
typer.echo(f"Loading SSH key from {ssh_key_file}...")
|
typer.echo(f"Loading SSH key from {ssh_key_file}...")
|
||||||
if not ssh_key_file.exists():
|
if not ssh_key_file.exists():
|
||||||
msg = f"{ssh_key_file} does not exist. "
|
msg = f"{ssh_key_file} does not exist. "
|
||||||
|
@ -203,7 +200,8 @@ def launch(
|
||||||
|
|
||||||
@server_cli.command()
|
@server_cli.command()
|
||||||
def topup(
|
def topup(
|
||||||
hostname: str,
|
hostname: str = "",
|
||||||
|
machine_id: str = "",
|
||||||
days: int = typer.Option(...),
|
days: int = typer.Option(...),
|
||||||
token: str = DEFAULT_TOKEN,
|
token: str = DEFAULT_TOKEN,
|
||||||
quote: bool = typer.Option(True, help="Require manual price confirmation."),
|
quote: bool = typer.Option(True, help="Require manual price confirmation."),
|
||||||
|
@ -212,15 +210,10 @@ def topup(
|
||||||
Extend an existing SporeStack server's lifetime.
|
Extend an existing SporeStack server's lifetime.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not machine_exists(hostname):
|
machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token)
|
||||||
typer.echo(f"{hostname} does not exist.")
|
|
||||||
raise typer.Exit(code=1)
|
|
||||||
|
|
||||||
_token = load_token(token)
|
_token = load_token(token)
|
||||||
|
|
||||||
machine_info = get_machine_info(hostname)
|
|
||||||
machine_id = machine_info["machine_id"]
|
|
||||||
|
|
||||||
if quote:
|
if quote:
|
||||||
response = api_client.topup(
|
response = api_client.topup(
|
||||||
machine_id=machine_id,
|
machine_id=machine_id,
|
||||||
|
@ -310,14 +303,17 @@ def pretty_machine_info(info: Dict[str, Any]) -> str:
|
||||||
|
|
||||||
|
|
||||||
@server_cli.command(name="list")
|
@server_cli.command(name="list")
|
||||||
def server_list(token: str = DEFAULT_TOKEN) -> None:
|
def server_list(
|
||||||
|
token: str = DEFAULT_TOKEN,
|
||||||
|
local: bool = typer.Option(
|
||||||
|
True, help="List older servers not associated to token."
|
||||||
|
),
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
List all locally known servers and all servers under the given token.
|
List all locally known servers and all servers under the given token.
|
||||||
"""
|
"""
|
||||||
from .exceptions import SporeStackUserError
|
from .exceptions import SporeStackUserError
|
||||||
|
|
||||||
directory = server_info_path()
|
|
||||||
|
|
||||||
_token = load_token(token)
|
_token = load_token(token)
|
||||||
|
|
||||||
server_infos = api_client.servers_launched_from_token(
|
server_infos = api_client.servers_launched_from_token(
|
||||||
|
@ -325,10 +321,12 @@ def server_list(token: str = DEFAULT_TOKEN) -> None:
|
||||||
).servers
|
).servers
|
||||||
machine_id_hostnames = {}
|
machine_id_hostnames = {}
|
||||||
|
|
||||||
for hostname_json in os.listdir(directory):
|
if local:
|
||||||
hostname = hostname_json.split(".")[0]
|
directory = server_info_path()
|
||||||
saved_vm_info = get_machine_info(hostname)
|
for hostname_json in os.listdir(directory):
|
||||||
machine_id_hostnames[saved_vm_info["machine_id"]] = hostname
|
hostname = hostname_json.split(".")[0]
|
||||||
|
saved_vm_info = get_machine_info(hostname)
|
||||||
|
machine_id_hostnames[saved_vm_info["machine_id"]] = hostname
|
||||||
|
|
||||||
printed_machine_ids = []
|
printed_machine_ids = []
|
||||||
|
|
||||||
|
@ -360,124 +358,138 @@ def server_list(token: str = DEFAULT_TOKEN) -> None:
|
||||||
|
|
||||||
printed_machine_ids.append(info.machine_id)
|
printed_machine_ids.append(info.machine_id)
|
||||||
|
|
||||||
for hostname_json in os.listdir(directory):
|
if local:
|
||||||
hostname = hostname_json.split(".")[0]
|
for hostname_json in os.listdir(directory):
|
||||||
saved_vm_info = get_machine_info(hostname)
|
hostname = hostname_json.split(".")[0]
|
||||||
machine_id = saved_vm_info["machine_id"]
|
saved_vm_info = get_machine_info(hostname)
|
||||||
if machine_id in printed_machine_ids:
|
machine_id = saved_vm_info["machine_id"]
|
||||||
continue
|
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"],
|
||||||
api_endpoint=get_api_endpoint(),
|
api_endpoint=get_api_endpoint(),
|
||||||
)
|
)
|
||||||
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
|
||||||
typer.echo()
|
typer.echo()
|
||||||
typer.echo(pretty_machine_info(saved_vm_info))
|
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(
|
||||||
"%Y-%m-%d %H:%M:%S %z", time.localtime(expiration)
|
"%Y-%m-%d %H:%M:%S %z", time.localtime(expiration)
|
||||||
)
|
)
|
||||||
msg = hostname
|
msg = hostname
|
||||||
msg += f" expired ({expiration} {human_expiration}): "
|
msg += f" expired ({expiration} {human_expiration}): "
|
||||||
msg += str(e)
|
msg += str(e)
|
||||||
typer.echo(msg)
|
typer.echo(msg)
|
||||||
|
|
||||||
typer.echo()
|
typer.echo()
|
||||||
|
|
||||||
|
|
||||||
def machine_exists(hostname: str) -> bool:
|
def _get_machine_id(machine_id: str, hostname: str, token: str) -> str:
|
||||||
"""
|
usage = "--hostname *OR* --machine-id must be set."
|
||||||
Check if the VM's JSON exists locally.
|
|
||||||
"""
|
if machine_id != "" and hostname != "":
|
||||||
return server_info_path().joinpath(f"{hostname}.json").exists()
|
typer.echo(usage, err=True)
|
||||||
|
raise typer.Exit(code=2)
|
||||||
|
|
||||||
|
if machine_id != "":
|
||||||
|
return machine_id
|
||||||
|
|
||||||
|
if hostname == "":
|
||||||
|
typer.echo(usage, err=True)
|
||||||
|
raise typer.Exit(code=2)
|
||||||
|
|
||||||
|
try:
|
||||||
|
machine_id = get_machine_info(hostname)["machine_id"]
|
||||||
|
assert isinstance(machine_id, str)
|
||||||
|
return machine_id
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
_token = load_token(token)
|
||||||
|
|
||||||
|
for server in api_client.servers_launched_from_token(
|
||||||
|
token=_token, api_endpoint=get_api_endpoint()
|
||||||
|
).servers:
|
||||||
|
if server.hostname == hostname:
|
||||||
|
return server.machine_id
|
||||||
|
|
||||||
|
typer.echo(
|
||||||
|
f"Could not find any servers matching the hostname: {hostname}", err=True
|
||||||
|
)
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
|
||||||
@server_cli.command()
|
@server_cli.command()
|
||||||
def get_attribute(hostname: str, attribute: str) -> None:
|
def info(hostname: str = "", machine_id: str = "", token: str = DEFAULT_TOKEN) -> None:
|
||||||
"""
|
|
||||||
Returns an attribute about the VM.
|
|
||||||
"""
|
|
||||||
machine_info = get_machine_info(hostname)
|
|
||||||
typer.echo(machine_info[attribute])
|
|
||||||
|
|
||||||
|
|
||||||
@server_cli.command()
|
|
||||||
def info(hostname: str) -> None:
|
|
||||||
"""
|
"""
|
||||||
Info on the VM
|
Info on the VM
|
||||||
"""
|
"""
|
||||||
machine_info = get_machine_info(hostname)
|
machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token)
|
||||||
machine_id = machine_info["machine_id"]
|
|
||||||
typer.echo(
|
typer.echo(
|
||||||
api_client.info(machine_id=machine_id, api_endpoint=get_api_endpoint()).json()
|
api_client.info(machine_id=machine_id, api_endpoint=get_api_endpoint()).json()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@server_cli.command()
|
@server_cli.command()
|
||||||
def start(hostname: str) -> None:
|
def start(hostname: str = "", machine_id: str = "", token: str = DEFAULT_TOKEN) -> None:
|
||||||
"""
|
"""
|
||||||
Boots the VM.
|
Boots the VM.
|
||||||
"""
|
"""
|
||||||
machine_info = get_machine_info(hostname)
|
machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token)
|
||||||
machine_id = machine_info["machine_id"]
|
|
||||||
api_client.start(machine_id=machine_id, api_endpoint=get_api_endpoint())
|
api_client.start(machine_id=machine_id, api_endpoint=get_api_endpoint())
|
||||||
typer.echo(f"{hostname} started.")
|
typer.echo(f"{hostname} started.")
|
||||||
|
|
||||||
|
|
||||||
@server_cli.command()
|
@server_cli.command()
|
||||||
def stop(hostname: str) -> None:
|
def stop(hostname: str = "", machine_id: str = "", token: str = DEFAULT_TOKEN) -> None:
|
||||||
"""
|
"""
|
||||||
Immediately shuts down the VM.
|
Immediately shuts down the VM.
|
||||||
"""
|
"""
|
||||||
machine_info = get_machine_info(hostname)
|
machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token)
|
||||||
machine_id = machine_info["machine_id"]
|
|
||||||
api_client.stop(machine_id=machine_id, api_endpoint=get_api_endpoint())
|
api_client.stop(machine_id=machine_id, api_endpoint=get_api_endpoint())
|
||||||
typer.echo(f"{hostname} stopped.")
|
typer.echo(f"{hostname} stopped.")
|
||||||
|
|
||||||
|
|
||||||
@server_cli.command()
|
@server_cli.command()
|
||||||
def destroy(hostname: str) -> None:
|
def destroy(
|
||||||
|
hostname: str = "", machine_id: str = "", token: str = DEFAULT_TOKEN
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Deletes/destroys the VM before expiration (no refunds/credits)
|
Deletes/destroys the VM before expiration (no refunds/credits)
|
||||||
"""
|
"""
|
||||||
_destroy(hostname)
|
machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token)
|
||||||
|
|
||||||
|
|
||||||
@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.destroy(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(missing_ok=True)
|
server_info_path().joinpath(f"{hostname}.json").unlink(missing_ok=True)
|
||||||
typer.echo(f"{hostname} was destroyed.")
|
typer.echo(f"{machine_id} was destroyed.")
|
||||||
|
|
||||||
|
|
||||||
@server_cli.command()
|
@server_cli.command()
|
||||||
def rebuild(hostname: str) -> None:
|
def forget(
|
||||||
|
hostname: str = "", machine_id: str = "", token: str = DEFAULT_TOKEN
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Forget about a deleted server so that it doesn't show up in server list.
|
||||||
|
"""
|
||||||
|
machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token)
|
||||||
|
api_client.forget(machine_id=machine_id, api_endpoint=get_api_endpoint())
|
||||||
|
typer.echo(f"{machine_id} was forgotten.")
|
||||||
|
|
||||||
|
|
||||||
|
@server_cli.command()
|
||||||
|
def rebuild(
|
||||||
|
hostname: str = "", machine_id: str = "", token: str = DEFAULT_TOKEN
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Rebuilds the VM with the operating system and SSH key given at launch time.
|
Rebuilds the VM with the operating system and SSH key given at launch time.
|
||||||
|
|
||||||
Will take a couple minutes to complete after the request is made.
|
Will take a couple minutes to complete after the request is made.
|
||||||
"""
|
"""
|
||||||
machine_info = get_machine_info(hostname)
|
machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token)
|
||||||
machine_id = machine_info["machine_id"]
|
|
||||||
api_client.rebuild(machine_id=machine_id, api_endpoint=get_api_endpoint())
|
api_client.rebuild(machine_id=machine_id, api_endpoint=get_api_endpoint())
|
||||||
typer.echo(f"{hostname} rebuilding.")
|
typer.echo(f"{hostname} rebuilding.")
|
||||||
|
|
||||||
|
@ -487,7 +499,21 @@ def flavors() -> None:
|
||||||
"""
|
"""
|
||||||
Returns available flavors.
|
Returns available flavors.
|
||||||
"""
|
"""
|
||||||
typer.echo(api_client.flavors(api_endpoint=get_api_endpoint()))
|
flavors = api_client.flavors(api_endpoint=get_api_endpoint()).flavors
|
||||||
|
for flavor in flavors:
|
||||||
|
typer.echo(f"{flavor}: {flavors[flavor]}")
|
||||||
|
|
||||||
|
|
||||||
|
@server_cli.command()
|
||||||
|
def operating_systems() -> None:
|
||||||
|
"""
|
||||||
|
Returns available operating systems.
|
||||||
|
"""
|
||||||
|
os_list = api_client.operating_systems(
|
||||||
|
api_endpoint=get_api_endpoint()
|
||||||
|
).operating_systems
|
||||||
|
for operating_system in os_list:
|
||||||
|
typer.echo(operating_system)
|
||||||
|
|
||||||
|
|
||||||
def load_token(token: str) -> str:
|
def load_token(token: str) -> str:
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import pytest
|
||||||
|
import typer
|
||||||
from _pytest.monkeypatch import MonkeyPatch
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
from typer.testing import CliRunner
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
|
@ -42,3 +44,15 @@ def test_cli_api_endpoint(monkeypatch: MonkeyPatch) -> None:
|
||||||
result = runner.invoke(cli.cli, ["api-endpoint"])
|
result = runner.invoke(cli.cli, ["api-endpoint"])
|
||||||
assert result.output == TOR_ENDPOINT + " using socks5h://127.0.0.1:1337\n"
|
assert result.output == TOR_ENDPOINT + " using socks5h://127.0.0.1:1337\n"
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_machine_id() -> None:
|
||||||
|
assert cli._get_machine_id("machine_id", "", "token") == "machine_id"
|
||||||
|
|
||||||
|
# machine_id and hostname set
|
||||||
|
with pytest.raises(typer.Exit):
|
||||||
|
cli._get_machine_id("machine_id", "hostname", "token")
|
||||||
|
|
||||||
|
# Neither is set
|
||||||
|
with pytest.raises(typer.Exit):
|
||||||
|
cli._get_machine_id("", "", "token")
|
||||||
|
|
Loading…
Reference in New Issue