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]
|
||||
|
||||
## [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]
|
||||
|
||||
### Added
|
||||
|
||||
- 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]
|
||||
|
||||
### Changed
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
__all__ = ["api", "api_client", "exceptions"]
|
||||
|
||||
__version__ = "6.2.0"
|
||||
__version__ = "7.0.0"
|
||||
|
|
|
@ -168,3 +168,11 @@ class Flavors:
|
|||
|
||||
class Response(BaseModel):
|
||||
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
|
||||
|
||||
|
||||
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(
|
||||
token: str,
|
||||
dollars: int,
|
||||
|
|
|
@ -60,10 +60,11 @@ os.umask(0o0077)
|
|||
|
||||
cli = typer.Typer(help=HELP)
|
||||
|
||||
token_cli = typer.Typer()
|
||||
HOME = Path(_home)
|
||||
|
||||
token_cli = typer.Typer(help="Commands to interact with SporeStack tokens")
|
||||
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")
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
@ -105,7 +106,7 @@ Press ctrl+c to abort."""
|
|||
|
||||
@server_cli.command()
|
||||
def launch(
|
||||
hostname: str,
|
||||
hostname: str = "",
|
||||
days: int = typer.Option(...),
|
||||
operating_system: str = typer.Option(...),
|
||||
ssh_key_file: Path = DEFAULT_SSH_KEY_FILE,
|
||||
|
@ -124,10 +125,6 @@ def launch(
|
|||
typer.echo(f"Launching server with token {token}...", err=True)
|
||||
_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}...")
|
||||
if not ssh_key_file.exists():
|
||||
msg = f"{ssh_key_file} does not exist. "
|
||||
|
@ -203,7 +200,8 @@ def launch(
|
|||
|
||||
@server_cli.command()
|
||||
def topup(
|
||||
hostname: str,
|
||||
hostname: str = "",
|
||||
machine_id: str = "",
|
||||
days: int = typer.Option(...),
|
||||
token: str = DEFAULT_TOKEN,
|
||||
quote: bool = typer.Option(True, help="Require manual price confirmation."),
|
||||
|
@ -212,15 +210,10 @@ def topup(
|
|||
Extend an existing SporeStack server's lifetime.
|
||||
"""
|
||||
|
||||
if not machine_exists(hostname):
|
||||
typer.echo(f"{hostname} does not exist.")
|
||||
raise typer.Exit(code=1)
|
||||
machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token)
|
||||
|
||||
_token = load_token(token)
|
||||
|
||||
machine_info = get_machine_info(hostname)
|
||||
machine_id = machine_info["machine_id"]
|
||||
|
||||
if quote:
|
||||
response = api_client.topup(
|
||||
machine_id=machine_id,
|
||||
|
@ -310,14 +303,17 @@ def pretty_machine_info(info: Dict[str, Any]) -> str:
|
|||
|
||||
|
||||
@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.
|
||||
"""
|
||||
from .exceptions import SporeStackUserError
|
||||
|
||||
directory = server_info_path()
|
||||
|
||||
_token = load_token(token)
|
||||
|
||||
server_infos = api_client.servers_launched_from_token(
|
||||
|
@ -325,10 +321,12 @@ def server_list(token: str = DEFAULT_TOKEN) -> None:
|
|||
).servers
|
||||
machine_id_hostnames = {}
|
||||
|
||||
for hostname_json in os.listdir(directory):
|
||||
hostname = hostname_json.split(".")[0]
|
||||
saved_vm_info = get_machine_info(hostname)
|
||||
machine_id_hostnames[saved_vm_info["machine_id"]] = hostname
|
||||
if local:
|
||||
directory = server_info_path()
|
||||
for hostname_json in os.listdir(directory):
|
||||
hostname = hostname_json.split(".")[0]
|
||||
saved_vm_info = get_machine_info(hostname)
|
||||
machine_id_hostnames[saved_vm_info["machine_id"]] = hostname
|
||||
|
||||
printed_machine_ids = []
|
||||
|
||||
|
@ -360,124 +358,138 @@ def server_list(token: str = DEFAULT_TOKEN) -> None:
|
|||
|
||||
printed_machine_ids.append(info.machine_id)
|
||||
|
||||
for hostname_json in os.listdir(directory):
|
||||
hostname = hostname_json.split(".")[0]
|
||||
saved_vm_info = get_machine_info(hostname)
|
||||
machine_id = saved_vm_info["machine_id"]
|
||||
if machine_id in printed_machine_ids:
|
||||
continue
|
||||
if local:
|
||||
for hostname_json in os.listdir(directory):
|
||||
hostname = hostname_json.split(".")[0]
|
||||
saved_vm_info = get_machine_info(hostname)
|
||||
machine_id = saved_vm_info["machine_id"]
|
||||
if machine_id in printed_machine_ids:
|
||||
continue
|
||||
|
||||
try:
|
||||
upstream_vm_info = api_client.info(
|
||||
machine_id=saved_vm_info["machine_id"],
|
||||
api_endpoint=get_api_endpoint(),
|
||||
)
|
||||
saved_vm_info["expiration"] = upstream_vm_info.expiration
|
||||
saved_vm_info["running"] = upstream_vm_info.running
|
||||
typer.echo()
|
||||
typer.echo(pretty_machine_info(saved_vm_info))
|
||||
except SporeStackUserError as e:
|
||||
expiration = saved_vm_info["expiration"]
|
||||
human_expiration = time.strftime(
|
||||
"%Y-%m-%d %H:%M:%S %z", time.localtime(expiration)
|
||||
)
|
||||
msg = hostname
|
||||
msg += f" expired ({expiration} {human_expiration}): "
|
||||
msg += str(e)
|
||||
typer.echo(msg)
|
||||
try:
|
||||
upstream_vm_info = api_client.info(
|
||||
machine_id=saved_vm_info["machine_id"],
|
||||
api_endpoint=get_api_endpoint(),
|
||||
)
|
||||
saved_vm_info["expiration"] = upstream_vm_info.expiration
|
||||
saved_vm_info["running"] = upstream_vm_info.running
|
||||
typer.echo()
|
||||
typer.echo(pretty_machine_info(saved_vm_info))
|
||||
except SporeStackUserError as e:
|
||||
expiration = saved_vm_info["expiration"]
|
||||
human_expiration = time.strftime(
|
||||
"%Y-%m-%d %H:%M:%S %z", time.localtime(expiration)
|
||||
)
|
||||
msg = hostname
|
||||
msg += f" expired ({expiration} {human_expiration}): "
|
||||
msg += str(e)
|
||||
typer.echo(msg)
|
||||
|
||||
typer.echo()
|
||||
|
||||
|
||||
def machine_exists(hostname: str) -> bool:
|
||||
"""
|
||||
Check if the VM's JSON exists locally.
|
||||
"""
|
||||
return server_info_path().joinpath(f"{hostname}.json").exists()
|
||||
def _get_machine_id(machine_id: str, hostname: str, token: str) -> str:
|
||||
usage = "--hostname *OR* --machine-id must be set."
|
||||
|
||||
if machine_id != "" and hostname != "":
|
||||
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()
|
||||
def get_attribute(hostname: str, attribute: str) -> 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:
|
||||
def info(hostname: str = "", machine_id: str = "", token: str = DEFAULT_TOKEN) -> None:
|
||||
"""
|
||||
Info on the VM
|
||||
"""
|
||||
machine_info = get_machine_info(hostname)
|
||||
machine_id = machine_info["machine_id"]
|
||||
machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token)
|
||||
typer.echo(
|
||||
api_client.info(machine_id=machine_id, api_endpoint=get_api_endpoint()).json()
|
||||
)
|
||||
|
||||
|
||||
@server_cli.command()
|
||||
def start(hostname: str) -> None:
|
||||
def start(hostname: str = "", machine_id: str = "", token: str = DEFAULT_TOKEN) -> None:
|
||||
"""
|
||||
Boots the VM.
|
||||
"""
|
||||
machine_info = get_machine_info(hostname)
|
||||
machine_id = machine_info["machine_id"]
|
||||
machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token)
|
||||
api_client.start(machine_id=machine_id, api_endpoint=get_api_endpoint())
|
||||
typer.echo(f"{hostname} started.")
|
||||
|
||||
|
||||
@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.
|
||||
"""
|
||||
machine_info = get_machine_info(hostname)
|
||||
machine_id = machine_info["machine_id"]
|
||||
machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token)
|
||||
api_client.stop(machine_id=machine_id, api_endpoint=get_api_endpoint())
|
||||
typer.echo(f"{hostname} stopped.")
|
||||
|
||||
|
||||
@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)
|
||||
"""
|
||||
_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"]
|
||||
machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token)
|
||||
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(missing_ok=True)
|
||||
typer.echo(f"{hostname} was destroyed.")
|
||||
typer.echo(f"{machine_id} was destroyed.")
|
||||
|
||||
|
||||
@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.
|
||||
|
||||
Will take a couple minutes to complete after the request is made.
|
||||
"""
|
||||
machine_info = get_machine_info(hostname)
|
||||
machine_id = machine_info["machine_id"]
|
||||
machine_id = _get_machine_id(machine_id=machine_id, hostname=hostname, token=token)
|
||||
api_client.rebuild(machine_id=machine_id, api_endpoint=get_api_endpoint())
|
||||
typer.echo(f"{hostname} rebuilding.")
|
||||
|
||||
|
@ -487,7 +499,21 @@ def flavors() -> None:
|
|||
"""
|
||||
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:
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import pytest
|
||||
import typer
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
from typer.testing import CliRunner
|
||||
|
||||
|
@ -42,3 +44,15 @@ def test_cli_api_endpoint(monkeypatch: MonkeyPatch) -> None:
|
|||
result = runner.invoke(cli.cli, ["api-endpoint"])
|
||||
assert result.output == TOR_ENDPOINT + " using socks5h://127.0.0.1:1337\n"
|
||||
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