|
|
|
@ -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
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
@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])
|
|
|
|
|
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 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)
|
|
|
|
|
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"{machine_id} was destroyed.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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:
|
|
|
|
|
def forget(
|
|
|
|
|
hostname: str = "", machine_id: str = "", token: str = DEFAULT_TOKEN
|
|
|
|
|
) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Deletes the VM before expiration (no refunds/credits)
|
|
|
|
|
Forget about a deleted server so that it doesn't show up in server list.
|
|
|
|
|
"""
|
|
|
|
|
machine_info = get_machine_info(hostname)
|
|
|
|
|
machine_id = machine_info["machine_id"]
|
|
|
|
|
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.")
|
|
|
|
|
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) -> None:
|
|
|
|
|
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:
|
|
|
|
|